View file rusnet-interactive-map/classes/class-shortcodes.php

File size: 26.04Kb
<?php
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class Rusnetim_Shortcodes {

    private static $maps_count = 0;
    private static $marker_count = 0;
    private static $current_map_index = 0;

    public function __construct() {
        add_shortcode( 'rusnetim_map', [ $this, 'render_map' ] );
        add_shortcode( 'rusnetim_marker', [ $this, 'render_marker' ] );
    }

    public function render_map( $atts, $content = '' ) {
        $current_map_index = self::$maps_count;
        self::set_current_map_index( $current_map_index );

        $defaults = Rusnetim_Options::get_all();

        $atts = shortcode_atts(
            [
                'center'        => $defaults['center_map_option'],
                'zoom'          => $defaults['zoom_map_option'],
                'type'          => 'map',
                'height'        => $defaults['height_map_option'],
                'controls'      => $defaults['controls_map_option'],
                'scrollzoom'    => ( $defaults['wheelzoom_map_option'] === 'on' ) ? '1' : '0',
                'mobiledrag'    => ( $defaults['mobiledrag_map_option'] === 'on' ) ? '1' : '0',
                'container'     => '',
                'cluster'       => ( $defaults['cluster_map_option'] === 'on' ) ? '1' : '0',
                'clustergrid'   => $defaults['cluster_grid_option'],
                'clustercolor'  => $defaults['cluster_color_option'],
                'markers'       => '',
                'category'      => '',
                'marker_id'     => '',
                'infowidth'     => $defaults['info_panel_width'],
                'infoside'      => $defaults['info_panel_side'],
                'titleposition' => $defaults['info_panel_title_position'],
                'title_tag'     => $defaults['info_title_tag'],
                'title_font_size' => $defaults['info_title_font_size'],
                'title_color'   => $defaults['info_title_color'],
                'infobgcolor'   => $defaults['info_panel_bg_color'],
            ],
            $atts
        );

        // ADDED FILTER: allow filtering of shortcode attributes
        $atts = apply_filters( 'rusnetim_shortcode_atts', $atts, $content );

        if ( $atts['height'] === '' ) {
            $atts['height'] = $defaults['height_map_option'];
        }
        if ( ! preg_match( '/^\d+(\.\d+)?(rem|em|px|vh)$/', $atts['height'] ) ) {
            $atts['height'] = '22rem';
        }

        $category_filter = '';

        $markers_data = [];

        if ( ! empty( $atts['marker_id'] ) && is_numeric( $atts['marker_id'] ) ) {
            $single_id = intval( $atts['marker_id'] );
            $marker_data = $this->prepare_marker_data_from_post( $single_id );
            if ( $marker_data ) {
                $markers_data[] = $marker_data;
            }
        } else {
            if ( ! empty( $atts['markers'] ) && 'all' === $atts['markers'] ) {
                $args = [
                    'post_type'      => 'rusnetim_marker',
                    'posts_per_page' => -1,
                    'post_status'    => 'publish',
                    'orderby'        => 'title',
                    'order'          => 'ASC',
                    'meta_query'     => [
                        'relation' => 'OR',
                        [
                            'key'     => '_marker_visible',
                            'compare' => 'NOT EXISTS',
                        ],
                        [
                            'key'     => '_marker_visible',
                            'value'   => '0',
                            'compare' => '!=',
                        ],
                    ],
                ];
                $db_markers = get_posts( $args );
                foreach ( $db_markers as $marker ) {
                    $marker_data = $this->prepare_marker_data_from_post( $marker->ID );
                    if ( $marker_data ) {
                        $markers_data[] = $marker_data;
                    }
                }
            } elseif ( ! empty( $atts['category'] ) ) {
                $args = [
                    'post_type'      => 'rusnetim_marker',
                    'posts_per_page' => -1,
                    'post_status'    => 'publish',
                    'tax_query'      => [
                        [
                            'taxonomy' => 'rusnetim_category',
                            'field'    => 'slug',
                            'terms'    => explode( ',', $atts['category'] ),
                        ],
                    ],
                    'meta_query'     => [
                        'relation' => 'OR',
                        [
                            'key'     => '_marker_visible',
                            'compare' => 'NOT EXISTS',
                        ],
                        [
                            'key'     => '_marker_visible',
                            'value'   => '0',
                            'compare' => '!=',
                        ],
                    ],
                ];
                $db_markers = get_posts( $args );
                foreach ( $db_markers as $marker ) {
                    $marker_data = $this->prepare_marker_data_from_post( $marker->ID );
                    if ( $marker_data ) {
                        $markers_data[] = $marker_data;
                    }
                }
            }

            if ( ! empty( $content ) ) {
                $pattern = get_shortcode_regex( [ 'rusnetim_marker' ] );
                if ( preg_match_all( '/' . $pattern . '/s', $content, $matches ) ) {
                    foreach ( $matches[0] as $shortcode_text ) {
                        $raw_atts = shortcode_parse_atts( $shortcode_text );
                        if ( isset( $raw_atts[0] ) ) unset( $raw_atts[0] );
                        $marker_data = $this->prepare_marker_data_from_shortcode_atts( $raw_atts );
                        if ( $marker_data ) {
                            $markers_data[] = $marker_data;
                        }
                    }
                }
            }
        }

        if ( $atts['center'] === $defaults['center_map_option'] && ! empty( $markers_data ) ) {
            $first_marker = reset( $markers_data );
            if ( isset( $first_marker['coord'] ) && ! empty( $first_marker['coord'] ) ) {
                $atts['center'] = $first_marker['coord'];
            }
        }

        $safe_center   = Rusnetim_Options::sanitize_coords( $atts['center'] );
        $safe_zoom     = Rusnetim_Options::sanitize_zoom( $atts['zoom'] );
        $safe_type     = Rusnetim_Options::sanitize_map_type( $atts['type'] );
        $controls_array = Rusnetim_Options::sanitize_controls( $atts['controls'] );

        $infoside = in_array( $atts['infoside'], [ 'right', 'left', 'modal' ] ) ? $atts['infoside'] : 'right';
        $infowidth = $atts['infowidth'];
        if ( ! preg_match( '/^\d+(\.\d+)?(px|%|em|rem|vw|vh)$/', $infowidth ) ) {
            $infowidth = '300px';
        }

        $atts['container'] = trim( $atts['container'] );
        if ( '' !== $atts['container'] ) {
            $mapcontainer = preg_replace( '/[^a-zA-Z0-9_\-]/', '', $atts['container'] );
            if ( preg_match( '/^[0-9]/', $mapcontainer ) ) {
                $mapcontainer = 'rusnetim-map-' . $mapcontainer;
            }
        } else {
            $mapcontainer = 'rusnetim-map' . $current_map_index;
        }
        $info_panel_id = 'rusnetim-info-' . $current_map_index;

        $map_data = [
            'map_index'       => $current_map_index,
            'container'       => $mapcontainer,
            'info_panel_id'   => $info_panel_id,
            'center'          => array_map( 'floatval', explode( ',', $safe_center ) ),
            'zoom'            => (int) $safe_zoom,
            'type'            => $safe_type,
            'controls'        => $controls_array,
            'scrollzoom'      => $atts['scrollzoom'],
            'mobiledrag'      => $atts['mobiledrag'],
            'cluster'         => $atts['cluster'],
            'clustergrid'     => (int) $atts['clustergrid'],
            'clustercolor'    => $atts['clustercolor'],
            'titleposition'   => $atts['titleposition'],
            'title_tag'       => $atts['title_tag'],
            'title_font_size' => $atts['title_font_size'],
            'title_color'     => $atts['title_color'],
            'cluster_list_title' => __( 'Organizations in this building', 'rusnet-interactive-map' ),
            'unnamed'         => __( 'Unnamed', 'rusnet-interactive-map' ),
            'markers'         => $markers_data,
            'infoside'        => $infoside,
            'infowidth'       => $infowidth,
        ];

        $api_key = $defaults['apikey_map_option'];
        $locale = Rusnetim_Options::sanitize_locale( get_locale() );
        wp_enqueue_script( 'yandex-maps-api', Rusnetim_API::get_api_url( $locale, $api_key ), [], null, true );

        if ( ! wp_script_is( 'rusnetim-front', 'registered' ) ) {
            wp_register_script(
                'rusnetim-front',
                RUSNETIM_PLUGIN_URL . 'assets/js/frontend/map-init.js',
                [],
                RUSNETIM_VERSION,
                true
            );
        }

        wp_localize_script( 'rusnetim-front', 'rusnetim_map_' . $current_map_index, $map_data );
        wp_enqueue_script( 'rusnetim-front' );

        $show_filter = empty( $atts['marker_id'] ) && empty( $atts['category'] );
        $categories = array();
        if ( $show_filter ) {
            $categories = $this->extract_categories_from_markers( $markers_data );
            $show_filter = count( $categories ) > 1;
        }

        if ( $show_filter ) {
            $cat_filter_css = '
.rusnetim-cat-btn[data-map="' . $current_map_index . '"] {
    display: inline-block;
    cursor: pointer;
    ' . ( ! empty( $defaults['cat_filter_button_font_size'] ) ? 'font-size: ' . esc_attr( $defaults['cat_filter_button_font_size'] ) . ';' : '' ) . '
    ' . ( ! empty( $defaults['cat_filter_button_color'] ) ? 'color: ' . esc_attr( $defaults['cat_filter_button_color'] ) . ';' : '' ) . '
    ' . ( ! empty( $defaults['cat_filter_button_bg_color'] ) ? 'background-color: ' . esc_attr( $defaults['cat_filter_button_bg_color'] ) . ';' : '' ) . '
    ' . ( ! empty( $defaults['cat_filter_button_border'] ) ? 'border: ' . esc_attr( $defaults['cat_filter_button_border'] ) . ';' : 'border: none;' ) . '
    ' . ( ! empty( $defaults['cat_filter_button_box_shadow'] ) ? 'box-shadow: ' . esc_attr( $defaults['cat_filter_button_box_shadow'] ) . ';' : '' ) . '
    ' . ( ! empty( $defaults['cat_filter_button_padding'] ) ? 'padding: ' . esc_attr( $defaults['cat_filter_button_padding'] ) . ';' : 'padding: 6px 12px;' ) . '
    ' . ( ! empty( $defaults['cat_filter_button_border_radius'] ) ? 'border-radius: ' . esc_attr( $defaults['cat_filter_button_border_radius'] ) . ';' : 'border-radius: 4px;' ) . '
}
.rusnetim-cat-btn.active[data-map="' . $current_map_index . '"] {
    ' . ( ! empty( $defaults['cat_filter_button_active_color'] ) ? 'color: ' . esc_attr( $defaults['cat_filter_button_active_color'] ) . ';' : '' ) . '
    ' . ( ! empty( $defaults['cat_filter_button_active_bg_color'] ) ? 'background-color: ' . esc_attr( $defaults['cat_filter_button_active_bg_color'] ) . ';' : '' ) . '
    ' . ( ! empty( $defaults['cat_filter_button_active_border'] ) ? 'border: ' . esc_attr( $defaults['cat_filter_button_active_border'] ) . ';' : '' ) . '
}';
            wp_register_style( 'rusnetim-cat-filter', false );
            wp_enqueue_style( 'rusnetim-cat-filter' );
            wp_add_inline_style( 'rusnetim-cat-filter', $cat_filter_css );

            $tag = esc_html( $defaults['cat_filter_button_tag'] );
            $container_justify = esc_attr( $defaults['cat_filter_container_justify'] );
            $category_filter = '<div class="rusnetim-category-filter" style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; justify-content: ' . $container_justify . ';">';
            $category_filter .= '<' . $tag . ' class="rusnetim-cat-btn active" data-cat="all" data-map="' . $current_map_index . '">' . esc_html__( 'All', 'rusnet-interactive-map' ) . '</' . $tag . '>';
            foreach ( $categories as $slug => $name ) {
                $category_filter .= '<' . $tag . ' class="rusnetim-cat-btn" data-cat="' . esc_attr( $slug ) . '" data-map="' . $current_map_index . '">' . esc_html( $name ) . '</' . $tag . '>';
            }
            $category_filter .= '</div>';
        }

        $authorLinkTitle = __( 'Interactive Map plugin by ANO "Rusnet"', 'rusnet-interactive-map' );
        if ( isset( $defaults['authorlink_map_option'] ) && 'on' !== $defaults['authorlink_map_option'] ) {
            $authorlink = '<div style="position: relative; height: 0; margin-bottom: 0rem !important; margin-top:0 !important; overflow: visible; width: 100%; text-align: center; top: -32px;">'
                        . '<a href="https://rusnet.su" title="' . esc_attr( $authorLinkTitle ) . '" target="_blank" style="display: inline-block; -webkit-box-align: center; padding: 3.5px 5px; text-decoration: none !important; border-bottom: 0; border-radius: 3px; background-color: #fff; cursor: pointer; white-space: nowrap; box-shadow: 0 1px 2px 1px rgba(0,0,0,.15),0 2px 5px -3px rgba(0,0,0,.15);">'
                        . '<img src="' . RUSNETIM_PLUGIN_URL . 'assets/js/img/placeholder.svg" alt="" style="width: 17px; height: 17px; margin: 0; display: block;" /></a></div>';
        } else {
            $authorlink = '';
        }

        $output = '';

        if ( ! empty( $category_filter ) ) {
            $output .= $category_filter;
        }

        $info_panel_html = apply_filters( 'rusnetim_info_panel_html', '', $mapcontainer, $info_panel_id, $infoside, $infowidth, $atts );
        
        if ( '' === $atts['container'] ) {
            $output .= '<div id="' . esc_attr( $mapcontainer ) . '" style="position: relative; height: ' . esc_attr( $atts['height'] ) . '; margin-bottom: 0 !important;">';
        
            if ( ! empty( $info_panel_html ) ) {
                $output .= $info_panel_html;
            } else {
                $panel_style = 'display:none; position:absolute; top:0; ' . $infoside . ':0; width:' . esc_attr( $infowidth ) . '; height:100%; background:' . esc_attr( $atts['infobgcolor'] ) . '; border-' . ( $infoside === 'right' ? 'left' : 'right' ) . ':1px solid #ccc; box-shadow:' . ( $infoside === 'right' ? '-2px 0 5px rgba(0,0,0,0.1)' : '2px 0 5px rgba(0,0,0,0.1)' ) . '; z-index:1000; overflow-y:auto; padding:15px; box-sizing:border-box;';
                $output .= '<div id="' . esc_attr( $info_panel_id ) . '" class="rusnetim-info-panel" style="' . $panel_style . '"></div>';
            }
        
            $output .= '</div>' . $authorlink;
        } else {
        $output .= $authorlink;
        $inline_js = '
    document.addEventListener("DOMContentLoaded", function() {
        var container = document.getElementById("' . esc_js( $mapcontainer ) . '");
        if ( container && ! document.getElementById("' . esc_js( $info_panel_id ) . '") ) {
            var panel = document.createElement("div");
            panel.id = "' . esc_js( $info_panel_id ) . '";
            panel.className = "rusnetim-info-panel";
            panel.style.display = "none";
            panel.style.position = "absolute";
            panel.style.top = "0";
            panel.style.' . $infoside . ' = "0";
            panel.style.width = "' . esc_attr( $infowidth ) . '";
            panel.style.height = "100%";
            panel.style.background = "' . esc_attr( $atts['infobgcolor'] ) . '";
            panel.style.border' . ( $infoside === 'right' ? 'Left' : 'Right' ) . ' = "1px solid #ccc";
            panel.style.boxShadow = "' . ( $infoside === 'right' ? '-2px 0 5px rgba(0,0,0,0.1)' : '2px 0 5px rgba(0,0,0,0.1)' ) . '";
            panel.style.overflowY = "auto";
            panel.style.padding = "15px";
            panel.style.zIndex = "1000";
            panel.style.boxSizing = "border-box";
            container.style.position = "relative";
            container.appendChild(panel);
        }
    });';
        wp_add_inline_script( 'rusnetim-front', $inline_js );
    }

        self::$maps_count++;
        return $output;
    }

    public function render_marker( $atts ) {
        return ''; 
    }

    /**
     * Получить все мета-поля маркера из базы по ID (с проверкой статуса publish и видимости)
     */
    private function get_marker_post_data( $post_id ) {
        $post = get_post( $post_id );
        if ( ! $post || $post->post_type !== 'rusnetim_marker' || $post->post_status !== 'publish' ) {
            return null;
        }

        $visible = get_post_meta( $post_id, '_marker_visible', true );
        if ( '0' === $visible ) {
            return null;
        }

        $coord = get_post_meta( $post_id, '_marker_coord', true );
        $name = $post->post_title;
        $icon = get_post_meta( $post_id, '_marker_icon', true );
        $color = get_post_meta( $post_id, '_marker_color', true );
        $url = get_post_meta( $post_id, '_marker_url', true );
        $image = get_post_meta( $post_id, '_marker_image', true );
        $description = $post->post_content;
        $icon_width = get_post_meta( $post_id, '_marker_icon_width', true );
        $icon_height = get_post_meta( $post_id, '_marker_icon_height', true );
        $custom_icon = get_post_meta( $post_id, '_marker_custom_icon', true );
        if ( ! empty( $custom_icon ) ) {
            $icon = $custom_icon;
        }

        $gallery_ids = get_post_meta( $post_id, '_marker_gallery', true );
        $gallery_urls = array();
        if ( ! empty( $gallery_ids ) && is_array( $gallery_ids ) ) {
            foreach ( $gallery_ids as $id ) {
                $url_img = wp_get_attachment_image_url( $id, 'full' );
                if ( $url_img ) {
                    $gallery_urls[] = $url_img;
                }
            }
        }

        $terms = wp_get_post_terms( $post_id, 'rusnetim_category', [ 'fields' => 'slugs' ] );
        $category = ! empty( $terms ) ? $terms[0] : '';

        return [
            'coord'       => $coord,
            'name'        => $name,
            'icon'        => $icon,
            'color'       => $color,
            'url'         => $url,
            'category'    => $category,
            'image'       => $image,
            'description' => $description,
            'icon_width'  => $icon_width,
            'icon_height' => $icon_height,
            'gallery'     => $gallery_urls,
        ];
    }

    private function prepare_marker_data_from_shortcode_atts( $raw_atts ) {
        if ( ! empty( $raw_atts['id'] ) && is_numeric( $raw_atts['id'] ) ) {
            $post_id = intval( $raw_atts['id'] );
            return $this->prepare_marker_data_from_post( $post_id );
        }
    
        $default_atts = [
            'id'          => 0,
            'coord'       => '',
            'name'        => '',
            'color'       => '',
            'url'         => '',
            'icon'        => '',
            'category'    => '',
            'image'       => '',
            'description' => '',
            'icon_width'  => '',
            'icon_height' => '',
            'gallery'     => '',
        ];
    
        $all_atts = shortcode_atts( $default_atts, $raw_atts );
    
        if ( empty( $all_atts['coord'] ) ) {
            $defaults = Rusnetim_Options::get_all();
            $all_atts['coord'] = $defaults['center_map_option'];
        }
    
        if ( empty( $all_atts['coord'] ) ) {
            return null;
        }
    
        if ( ! empty( $all_atts['gallery'] ) && is_string( $all_atts['gallery'] ) ) {
            $decoded_json = base64_decode( $all_atts['gallery'] );
            if ( $decoded_json !== false ) {
                $decoded = json_decode( $decoded_json, true );
                $all_atts['gallery'] = is_array( $decoded ) ? $decoded : [];
            } else {
                $all_atts['gallery'] = [];
            }
        } elseif ( ! is_array( $all_atts['gallery'] ) ) {
            $all_atts['gallery'] = [];
        }
    
        return $this->normalize_marker_data( $all_atts );
    }

    private function prepare_marker_data_from_post( $post_id ) {
        $post_data = $this->get_marker_post_data( $post_id );
        if ( ! $post_data ) {
            return null;
        }

        if ( empty( $post_data['coord'] ) ) {
            $defaults = Rusnetim_Options::get_all();
            $post_data['coord'] = $defaults['center_map_option'];
        }

        return $this->normalize_marker_data( $post_data );
    }

    private function normalize_marker_data( $atts ) {
        $defaults = Rusnetim_Options::get_all();

        if ( ! empty( $atts['category'] ) ) {
            $term = get_term_by( 'slug', $atts['category'], 'rusnetim_category' );
            if ( $term && ! is_wp_error( $term ) ) {
                if ( empty( $atts['icon'] ) ) {
                    $cat_icon = get_term_meta( $term->term_id, 'category_icon', true );
                    if ( ! empty( $cat_icon ) ) $atts['icon'] = $cat_icon;
                }
                if ( empty( $atts['color'] ) ) {
                    $cat_color = get_term_meta( $term->term_id, 'category_color', true );
                    if ( ! empty( $cat_color ) ) $atts['color'] = $cat_color;
                }
                if ( empty( $atts['icon_width'] ) ) {
                    $cat_width = get_term_meta( $term->term_id, 'category_icon_width', true );
                    if ( ! empty( $cat_width ) ) $atts['icon_width'] = $cat_width;
                }
                if ( empty( $atts['icon_height'] ) ) {
                    $cat_height = get_term_meta( $term->term_id, 'category_icon_height', true );
                    if ( ! empty( $cat_height ) ) $atts['icon_height'] = $cat_height;
                }
            }
        }

        if ( empty( $atts['icon'] ) ) {
            if ( ! empty( $defaults['custom_icon_option'] ) ) {
                $atts['icon'] = $defaults['custom_icon_option'];
                if ( empty( $atts['icon_width'] ) ) $atts['icon_width'] = $defaults['custom_icon_width_option'] ?? '';
                if ( empty( $atts['icon_height'] ) ) $atts['icon_height'] = $defaults['custom_icon_height_option'] ?? '';
            } else {
                $atts['icon'] = $defaults['type_icon_option'];
            }
        }
        if ( empty( $atts['color'] ) ) {
            $atts['color'] = $defaults['color_icon_option'];
        }

        $icon = $atts['icon'];
        $icon_url = '';
        $icon_preset = '';
        $icon_size = [];

        if ( strpos( $icon, 'myset:' ) === 0 ) {
            $slug = substr( $icon, 6 );
            $my_iconset = $defaults['my_iconset'];
            if ( isset( $my_iconset[ $slug ] ) ) {
                $data = $my_iconset[ $slug ];
                $icon_url = $data['url'];
                if ( empty( $atts['icon_width'] ) && ! empty( $data['width'] ) ) {
                    $atts['icon_width'] = $data['width'];
                }
                if ( empty( $atts['icon_height'] ) && ! empty( $data['height'] ) ) {
                    $atts['icon_height'] = $data['height'];
                }
            } else {
                $icon_preset = $defaults['type_icon_option'];
            }
        } elseif ( is_numeric( $icon ) ) {
            $image_url = wp_get_attachment_image_url( intval( $icon ), 'full' );
            if ( $image_url ) {
                $icon_url = $image_url;
                $img_data = wp_get_attachment_image_src( intval( $icon ), 'full' );
                if ( $img_data ) {
                    if ( empty( $atts['icon_width'] ) ) $atts['icon_width'] = $img_data[1];
                    if ( empty( $atts['icon_height'] ) ) $atts['icon_height'] = $img_data[2];
                }
            } else {
                $icon_preset = $defaults['type_icon_option'];
            }
        } elseif ( filter_var( $icon, FILTER_VALIDATE_URL ) ) {
            $icon_url = $icon;
        } else {
            $icon_preset = $icon;
        }

        if ( $icon_url && $atts['icon_width'] && $atts['icon_height'] ) {
            $icon_size = [ (int) $atts['icon_width'], (int) $atts['icon_height'] ];
        }

        $hint = '';
        $content = '';
        if ( strpos( $icon_preset, 'Stretchy' ) !== false ) {
            $content = $atts['name'];
        } else {
            if ( in_array( $icon_preset, [ 'islands#blueIcon', 'islands#blueCircleIcon' ], true ) ) {
                $hint = $atts['name'];
                $content = $atts['name'] ? mb_substr( $atts['name'], 0, 1 ) : '';
            } else {
                $hint = $atts['name'];
            }
        }

        $link = $atts['url'];
        if ( is_numeric( $link ) ) {
            $link = get_permalink( intval( $link ) );
        }

        return [
            'coord'       => Rusnetim_Options::sanitize_coords( $atts['coord'] ),
            'name'        => sanitize_text_field( $atts['name'] ),
            'hint'        => $hint,
            'content'     => $content,
            'iconPreset'  => $icon_preset,
            'iconUrl'     => esc_url_raw( $icon_url ),
            'iconColor'   => sanitize_hex_color( $atts['color'] ) ?: '#1e98ff',
            'iconSize'    => $icon_size,
            'url'         => esc_url_raw( $link ),
            'category'    => sanitize_text_field( $atts['category'] ),
            'image'       => esc_url_raw( $atts['image'] ),
            'description' => wp_kses_post( $atts['description'] ),
            'gallery'     => array_map( 'esc_url_raw', $atts['gallery'] ),
        ];
    }

    private function extract_categories_from_markers( $markers ) {
        $slugs = [];
        foreach ( $markers as $m ) {
            if ( ! empty( $m['category'] ) ) {
                $slugs[] = $m['category'];
            }
        }
        $slugs = array_unique( $slugs );
        $result = [];
        foreach ( $slugs as $slug ) {
            $term = get_term_by( 'slug', $slug, 'rusnetim_category' );
            if ( $term && ! is_wp_error( $term ) ) {
                $result[ $slug ] = $term->name;
            } else {
                $result[ $slug ] = $slug;
            }
        }
        return $result;
    }

    public static function set_current_map_index( $index ) {
        self::$current_map_index = $index;
    }
}