Geoposicionador de cosas

This commit is contained in:
Jose Ibáñez
2026-02-13 13:14:41 +01:00
commit a17bd50744
10 changed files with 3440 additions and 0 deletions
@@ -0,0 +1,280 @@
<?php
/**
* Proveedor de datos para el mapa
*
* @package FP_Geo_Content
*/
if (!defined('ABSPATH')) {
exit;
}
class FP_Geo_Data_Provider {
/**
* Obtener marcadores para el mapa
*
* @param array $args Argumentos de configuración
* @return array
*/
public static function get_markers($args = []) {
$options = get_option('fp_geo_content_options', []);
$defaults = [
'post_types' => $options['post_types'] ?? [],
'lat_field' => $options['lat_field'] ?? 'latitud',
'lng_field' => $options['lng_field'] ?? 'longitud',
'taxonomies' => [],
'terms' => [],
'filter_combine' => $options['filter_combine'] ?? 'OR',
];
$args = wp_parse_args($args, $defaults);
if (empty($args['post_types'])) {
return [];
}
// Query args
$query_args = [
'post_type' => $args['post_types'],
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_query' => [
'relation' => 'AND',
[
'key' => $args['lat_field'],
'compare' => 'EXISTS',
],
[
'key' => $args['lng_field'],
'compare' => 'EXISTS',
],
[
'key' => $args['lat_field'],
'value' => '',
'compare' => '!=',
],
[
'key' => $args['lng_field'],
'value' => '',
'compare' => '!=',
],
],
];
// Filtros por taxonomía
if (!empty($args['terms']) && is_array($args['terms'])) {
$tax_query = ['relation' => $args['filter_combine']];
foreach ($args['terms'] as $taxonomy => $term_slugs) {
if (!empty($term_slugs)) {
$tax_query[] = [
'taxonomy' => $taxonomy,
'field' => 'slug',
'terms' => (array) $term_slugs,
];
}
}
if (count($tax_query) > 1) {
$query_args['tax_query'] = $tax_query;
}
}
$query = new WP_Query($query_args);
$markers = [];
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$post = get_post();
$marker = self::prepare_marker($post, $args);
if ($marker) {
$markers[] = $marker;
}
}
wp_reset_postdata();
}
return $markers;
}
/**
* Preparar datos de un marcador
*
* @param WP_Post $post
* @param array $args
* @return array|null
*/
private static function prepare_marker($post, $args) {
// Obtener coordenadas
$lat = get_field($args['lat_field'], $post->ID);
$lng = get_field($args['lng_field'], $post->ID);
// Si no hay coordenadas, intentar con get_post_meta
if (!$lat) {
$lat = get_post_meta($post->ID, $args['lat_field'], true);
}
if (!$lng) {
$lng = get_post_meta($post->ID, $args['lng_field'], true);
}
// Validar coordenadas
if (!$lat || !$lng || !is_numeric($lat) || !is_numeric($lng)) {
return null;
}
// Datos básicos
$marker = [
'id' => $post->ID,
'title' => html_entity_decode(get_the_title($post), ENT_QUOTES, 'UTF-8'),
'lat' => (float) $lat,
'lng' => (float) $lng,
'url' => get_permalink($post),
'post_type' => $post->post_type,
'excerpt' => wp_trim_words(get_the_excerpt($post), 20),
'thumbnail' => get_the_post_thumbnail_url($post, 'medium'),
];
// Obtener taxonomías
$options = get_option('fp_geo_content_options', []);
$filter_taxonomies = $options['filter_taxonomies'] ?? [];
$marker['taxonomies'] = [];
foreach ($filter_taxonomies as $taxonomy) {
$terms = wp_get_post_terms($post->ID, $taxonomy);
if (!is_wp_error($terms) && !empty($terms)) {
$marker['taxonomies'][$taxonomy] = [];
foreach ($terms as $term) {
$term_data = [
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
];
// Obtener campos ACF del término si existen (icono y color)
if (function_exists('get_field')) {
$icono = get_field('icono', $term);
$color = get_field('color', $term);
if ($icono) $term_data['icono'] = $icono;
if ($color) $term_data['color'] = $color;
}
$marker['taxonomies'][$taxonomy][] = $term_data;
}
}
}
// Campo es_piloto del post (para actuaciones)
$es_piloto = get_field('es_piloto', $post->ID);
if (!$es_piloto) {
$es_piloto = get_post_meta($post->ID, 'es_piloto', true);
}
if ($es_piloto) {
$marker['es_piloto'] = true;
}
// Campos adicionales comunes
$additional_fields = ['direccion', 'localidad', 'telefono', 'email', 'web'];
foreach ($additional_fields as $field) {
$value = get_field($field, $post->ID);
if (!$value) {
$value = get_post_meta($post->ID, $field, true);
}
if ($value) {
$marker[$field] = $value;
}
}
// Permitir filtrar los datos del marcador
return apply_filters('fp_geo_marker_data', $marker, $post);
}
/**
* Handler AJAX para obtener marcadores
*/
public static function ajax_get_markers() {
check_ajax_referer('fp_geo_nonce', 'nonce');
$post_types = isset($_POST['post_types']) ? array_map('sanitize_text_field', (array) $_POST['post_types']) : [];
$terms = isset($_POST['terms']) ? $_POST['terms'] : [];
// Sanitizar términos
$sanitized_terms = [];
if (is_array($terms)) {
foreach ($terms as $taxonomy => $slugs) {
$taxonomy = sanitize_text_field($taxonomy);
$sanitized_terms[$taxonomy] = array_map('sanitize_text_field', (array) $slugs);
}
}
$markers = self::get_markers([
'post_types' => $post_types,
'terms' => $sanitized_terms,
]);
wp_send_json_success(['markers' => $markers]);
}
/**
* Obtener términos disponibles para filtros
*
* @param array $post_types
* @param array $taxonomies
* @return array
*/
public static function get_filter_terms($post_types = [], $taxonomies = []) {
$result = [];
foreach ($taxonomies as $taxonomy) {
$tax_obj = get_taxonomy($taxonomy);
if (!$tax_obj) continue;
// Verificar que la taxonomía está asociada a alguno de los post types
$associated = false;
foreach ($post_types as $pt) {
if (in_array($pt, $tax_obj->object_type)) {
$associated = true;
break;
}
}
if (!$associated) continue;
$terms = get_terms([
'taxonomy' => $taxonomy,
'hide_empty' => true,
]);
if (!is_wp_error($terms) && !empty($terms)) {
$result[$taxonomy] = [
'label' => $tax_obj->label,
'terms' => [],
];
foreach ($terms as $term) {
$term_data = [
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
'count' => $term->count,
];
// Campos ACF del término (icono y color)
if (function_exists('get_field')) {
$icono = get_field('icono', $term);
$color = get_field('color', $term);
if ($icono) $term_data['icono'] = $icono;
if ($color) $term_data['color'] = $color;
}
$result[$taxonomy]['terms'][] = $term_data;
}
}
}
return $result;
}
}
@@ -0,0 +1,357 @@
<?php
/**
* Renderizador del mapa
*
* @package FP_Geo_Content
*/
if (!defined('ABSPATH')) {
exit;
}
class FP_Geo_Map_Renderer
{
/**
* Contador de instancias para IDs únicos
*/
private static $instance_count = 0;
/**
* Shortcode: [fp-geo-map]
*
* Atributos:
* - post_types: tipos de contenido separados por coma (usa config si no se especifica)
* - taxonomies: taxonomías para filtros separadas por coma
* - height: altura del mapa (default: 600px)
* - lat: latitud centro (usa config si no se especifica)
* - lng: longitud centro
* - zoom: nivel de zoom inicial
* - filters: true|false - mostrar filtros
* - detail: sidebar|modal - cómo mostrar el detalle
* - sidebar_position: left|right - posición del sidebar
* - legend: true|false - mostrar leyenda
* - show_detail_btn: true|false - mostrar botón de detalle
* - detail_btn_text: texto del botón
* - class: clases CSS adicionales
*/
public static function render_shortcode($atts)
{
$options = get_option('fp_geo_content_options', []);
$atts = shortcode_atts([
'post_types' => implode(',', $options['post_types'] ?? []),
'taxonomies' => implode(',', $options['filter_taxonomies'] ?? []),
'height' => '600px',
'lat' => $options['default_lat'] ?? '40.4168',
'lng' => $options['default_lng'] ?? '-3.7038',
'zoom' => $options['default_zoom'] ?? 12,
'filters' => 'true',
'detail' => $options['detail_display'] ?? 'sidebar',
'cluster' => $options['cluster_enabled'] ?? true,
'sidebar_position' => $options['sidebar_position'] ?? 'right',
'legend' => isset($options['show_legend']) ? ($options['show_legend'] ? 'true' : 'false') : 'false',
'show_detail_btn' => isset($options['show_detail_button']) ? ($options['show_detail_button'] ? 'true' : 'false') : 'true',
'detail_btn_text' => $options['detail_button_text'] ?? __('Ver detalle', 'fp-geo-content'),
'class' => '',
], $atts, 'fp-geo-map');
// Parsear valores
$post_types = array_filter(array_map('trim', explode(',', $atts['post_types'])));
$taxonomies = array_filter(array_map('trim', explode(',', $atts['taxonomies'])));
$show_filters = filter_var($atts['filters'], FILTER_VALIDATE_BOOLEAN);
$use_cluster = filter_var($atts['cluster'], FILTER_VALIDATE_BOOLEAN);
$show_legend = filter_var($atts['legend'], FILTER_VALIDATE_BOOLEAN);
$show_detail_btn = filter_var($atts['show_detail_btn'], FILTER_VALIDATE_BOOLEAN);
$sidebar_position = $atts['sidebar_position'];
if (empty($post_types)) {
return '<p class="fp-geo-error">' . __('No se han configurado tipos de contenido para el mapa.', 'fp-geo-content') . '</p>';
}
// Incrementar contador de instancias
self::$instance_count++;
$map_id = 'fp-geo-map-' . self::$instance_count;
// Cargar assets
wp_enqueue_style('fp-geo-content');
wp_enqueue_script('fp-geo-content');
// Obtener marcadores
$markers = FP_Geo_Data_Provider::get_markers([
'post_types' => $post_types,
]);
// Obtener términos para filtros
$filter_terms = [];
if ($show_filters && !empty($taxonomies)) {
$filter_terms = FP_Geo_Data_Provider::get_filter_terms($post_types, $taxonomies);
}
// Obtener configuración de tiles
$tile_provider = $options['tile_provider'] ?? 'carto_light';
$tile_providers = FP_Geo_Settings::get_tile_providers();
$tile_config = $tile_providers[$tile_provider] ?? $tile_providers['carto_light'];
// Obtener icono personalizado si existe
$marker_icon_id = $options['marker_icon'] ?? 0;
$marker_icon_url = $marker_icon_id ? wp_get_attachment_image_url($marker_icon_id, 'full') : '';
// Obtener etiquetas de los post types
$post_type_labels = [];
foreach ($post_types as $pt) {
$pt_obj = get_post_type_object($pt);
if ($pt_obj) {
$post_type_labels[$pt] = [
'singular' => $pt_obj->labels->singular_name,
'plural' => $pt_obj->labels->name,
];
}
}
// Obtener datos de leyenda si está habilitada
$legend_data = [];
$legend_taxonomy = $options['legend_taxonomy'] ?? '';
if ($show_legend && !empty($legend_taxonomy) && isset($filter_terms[$legend_taxonomy])) {
// Verificar si hay marcadores piloto (es_piloto está en los posts, no en los términos)
$has_pilot_markers = false;
foreach ($markers as $marker) {
if (isset($marker['es_piloto']) && $marker['es_piloto']) {
$has_pilot_markers = true;
break;
}
}
$legend_data = [
'taxonomy' => $legend_taxonomy,
'label' => $filter_terms[$legend_taxonomy]['label'],
'items' => $filter_terms[$legend_taxonomy]['terms'],
'has_pilots' => $has_pilot_markers,
];
}
// Configuración del mapa
$map_config = [
'mapId' => $map_id,
'center' => [(float) $atts['lat'], (float) $atts['lng']],
'zoom' => (int) $atts['zoom'],
'minZoom' => (int) ($options['min_zoom'] ?? 5),
'maxZoom' => (int) ($options['max_zoom'] ?? 18),
'markers' => $markers,
'filters' => $filter_terms,
'filterCombine' => $options['filter_combine'] ?? 'OR',
'detailDisplay' => $atts['detail'],
'clusterEnabled' => $use_cluster,
'tileUrl' => $tile_config['url'],
'tileAttribution' => $tile_config['attribution'],
'tileSubdomains' => $tile_config['subdomains'],
'defaultIcon' => FP_GEO_PLUGIN_URL . 'assets/img/marker-icon.png',
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('fp_geo_nonce'),
'postTypes' => $post_types,
'postTypeLabels' => $post_type_labels,
// Nuevas opciones de marcadores
'markerIcon' => $marker_icon_url,
'markerDefaultColor' => $options['marker_default_color'] ?? '#F97316',
'useCategoryColors' => isset($options['use_category_colors']) && $options['use_category_colors'],
'legendTaxonomy' => $legend_taxonomy,
// Nuevas opciones de scroll
'scrollWheelZoom' => $options['scroll_wheel_zoom'] ?? 'ctrl',
// Nuevas opciones de display
'sidebarPosition' => $sidebar_position,
'showLegend' => $show_legend,
'legendData' => $legend_data,
'showDetailButton' => $show_detail_btn,
'detailButtonText' => $atts['detail_btn_text'],
'i18n' => [
'loading' => __('Cargando...', 'fp-geo-content'),
'noResults' => __('No se encontraron resultados', 'fp-geo-content'),
'viewMore' => $atts['detail_btn_text'],
'close' => __('Cerrar', 'fp-geo-content'),
'clearFilters' => __('Limpiar filtros', 'fp-geo-content'),
'scrollZoomHint' => __('Usa Ctrl + scroll para hacer zoom', 'fp-geo-content'),
],
];
// Pasar configuración al JS
wp_localize_script('fp-geo-content', 'fpGeoConfig_' . self::$instance_count, $map_config);
ob_start();
$wrapper_classes = [
'fp-geo-wrapper',
'fp-geo-detail-' . esc_attr($atts['detail']),
'fp-geo-sidebar-' . esc_attr($sidebar_position),
esc_attr($atts['class']),
];
?>
<div id="<?php echo esc_attr($map_id); ?>-wrapper"
class="<?php echo esc_attr(implode(' ', array_filter($wrapper_classes))); ?>"
data-instance="<?php echo esc_attr(self::$instance_count); ?>">
<?php if ($show_filters && !empty($filter_terms)): ?>
<?php echo self::render_filters($map_id, $filter_terms); ?>
<?php endif; ?>
<div class="fp-geo-map-container">
<div id="<?php echo esc_attr($map_id); ?>"
class="fp-geo-map"
style="height: <?php echo esc_attr($atts['height']); ?>;"></div>
<?php if ($show_legend && !empty($legend_data)): ?>
<?php echo self::render_legend($map_id, $legend_data); ?>
<?php endif; ?>
<?php echo self::render_detail_panel($map_id, $atts['detail'], $show_detail_btn, $atts['detail_btn_text']); ?>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Renderizar panel de filtros
*/
private static function render_filters($map_id, $filter_terms)
{
ob_start();
?>
<div class="fp-geo-filters" data-map="<?php echo esc_attr($map_id); ?>">
<div class="fp-geo-filters-inner">
<?php foreach ($filter_terms as $taxonomy => $data): ?>
<div class="fp-geo-filter-group" data-taxonomy="<?php echo esc_attr($taxonomy); ?>">
<label class="fp-geo-filter-label"><?php echo esc_html($data['label']); ?>:</label>
<div class="fp-geo-filter-buttons">
<?php foreach ($data['terms'] as $term): ?>
<button type="button"
class="fp-geo-filter-btn"
data-slug="<?php echo esc_attr($term['slug']); ?>"
data-taxonomy="<?php echo esc_attr($taxonomy); ?>"
style="<?php echo isset($term['color']) ? '--filter-color: ' . esc_attr($term['color']) . ';' : ''; ?>">
<?php if (isset($term['icono'])): ?>
<img src="<?php echo esc_url($term['icono']); ?>" alt="" class="fp-geo-filter-icon">
<?php endif; ?>
<span><?php echo esc_html($term['name']); ?></span>
</button>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
<div class="fp-geo-filters-footer">
<span class="fp-geo-results-count">
<?php _e('Mostrando', 'fp-geo-content'); ?>
<strong class="fp-geo-results-number">0</strong>
<span class="fp-geo-results-label"><?php _e('resultados', 'fp-geo-content'); ?></span>
</span>
<button type="button" class="fp-geo-clear-filters">
<?php _e('Limpiar filtros', 'fp-geo-content'); ?>
</button>
</div>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Renderizar leyenda
*/
private static function render_legend($map_id, $legend_data)
{
if (empty($legend_data['items'])) {
return '';
}
$has_pilots = isset($legend_data['has_pilots']) && $legend_data['has_pilots'];
ob_start();
?>
<div id="<?php echo esc_attr($map_id); ?>-legend" class="fp-geo-legend">
<div class="fp-geo-legend-title"><?php _e('Leyenda', 'fp-geo-content'); ?></div>
<div class="fp-geo-legend-section">
<div class="fp-geo-legend-section-title"><?php _e('Iniciativas', 'fp-geo-content'); ?></div>
<div class="fp-geo-legend-items">
<?php foreach ($legend_data['items'] as $item):
$color = isset($item['color']) ? $item['color'] : '#F97316';
?>
<div class="fp-geo-legend-item">
<span class="fp-geo-legend-marker" style="background-color: <?php echo esc_attr($color); ?>;"></span>
<span class="fp-geo-legend-label"><?php echo esc_html($item['name']); ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
<?php if ($has_pilots): ?>
<div class="fp-geo-legend-section fp-geo-legend-pilot-section">
<div class="fp-geo-legend-items">
<div class="fp-geo-legend-item fp-geo-legend-pilot">
<span class="fp-geo-legend-marker-pilot" aria-hidden="true">
<svg width="22" height="28" viewBox="0 0 36 48">
<circle cx="18" cy="18" r="15" fill="#888" stroke="white" stroke-width="3"/>
<polygon points="18,42 24,30 12,30" fill="#888" stroke="white" stroke-width="2" stroke-linejoin="round"/>
<text x="18" y="22" text-anchor="middle" fill="white" font-size="14" font-weight="bold">★</text>
</svg>
</span>
<span class="fp-geo-legend-label"><?php _e('Bloque Piloto', 'fp-geo-content'); ?></span>
</div>
<p class="fp-geo-legend-pilot-desc">
<?php _e('Actuación piloto (transversal a cualquier iniciativa)', 'fp-geo-content'); ?>
</p>
</div>
</div>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
/**
* Renderizar panel de detalle
*/
private static function render_detail_panel($map_id, $display_type, $show_button = true, $button_text = '')
{
$class = $display_type === 'modal' ? 'fp-geo-modal' : 'fp-geo-sidebar';
$button_text = $button_text ?: __('Ver más', 'fp-geo-content');
ob_start();
?>
<div id="<?php echo esc_attr($map_id); ?>-detail" class="fp-geo-detail <?php echo esc_attr($class); ?>">
<button class="fp-geo-detail-close" aria-label="<?php _e('Cerrar', 'fp-geo-content'); ?>">×</button>
<div class="fp-geo-detail-content">
<div class="fp-geo-detail-header">
<div class="fp-geo-detail-taxonomies"></div>
</div>
<div class="fp-geo-detail-thumbnail"></div>
<h2 class="fp-geo-detail-title"></h2>
<div class="fp-geo-detail-excerpt"></div>
<div class="fp-geo-detail-meta">
<div class="fp-geo-detail-location"></div>
<div class="fp-geo-detail-contact"></div>
</div>
<?php if ($show_button): ?>
<div class="fp-geo-detail-footer">
<a href="#" class="fp-geo-detail-link fp-geo-btn" target="_blank">
<?php echo esc_html($button_text); ?>
</a>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($display_type === 'modal'): ?>
<div id="<?php echo esc_attr($map_id); ?>-overlay" class="fp-geo-overlay"></div>
<?php endif; ?>
<?php
return ob_get_clean();
}
}
+765
View File
@@ -0,0 +1,765 @@
<?php
/**
* Configuración del plugin
*
* @package FP_Geo_Content
*/
if (!defined('ABSPATH')) {
exit;
}
class FP_Geo_Settings {
/**
* Registrar settings
*/
public static function register() {
// Nada que hacer en init por ahora
}
/**
* Registrar ajustes de WordPress
*/
public static function register_settings() {
register_setting('fp_geo_content_options', 'fp_geo_content_options', [
'sanitize_callback' => [__CLASS__, 'sanitize_options'],
]);
// Sección: Tipos de contenido
add_settings_section(
'fp_geo_content_types',
__('Tipos de Contenido', 'fp-geo-content'),
[__CLASS__, 'section_content_types'],
'fp-geo-content'
);
add_settings_field(
'post_types',
__('Tipos de contenido a mostrar', 'fp-geo-content'),
[__CLASS__, 'field_post_types'],
'fp-geo-content',
'fp_geo_content_types'
);
add_settings_field(
'lat_field',
__('Campo de Latitud', 'fp-geo-content'),
[__CLASS__, 'field_lat'],
'fp-geo-content',
'fp_geo_content_types'
);
add_settings_field(
'lng_field',
__('Campo de Longitud', 'fp-geo-content'),
[__CLASS__, 'field_lng'],
'fp-geo-content',
'fp_geo_content_types'
);
// Sección: Configuración del mapa
add_settings_section(
'fp_geo_map_settings',
__('Configuración del Mapa', 'fp-geo-content'),
[__CLASS__, 'section_map_settings'],
'fp-geo-content'
);
add_settings_field(
'default_center',
__('Centro por defecto', 'fp-geo-content'),
[__CLASS__, 'field_default_center'],
'fp-geo-content',
'fp_geo_map_settings'
);
add_settings_field(
'zoom_levels',
__('Niveles de zoom', 'fp-geo-content'),
[__CLASS__, 'field_zoom_levels'],
'fp-geo-content',
'fp_geo_map_settings'
);
add_settings_field(
'tile_provider',
__('Proveedor de tiles', 'fp-geo-content'),
[__CLASS__, 'field_tile_provider'],
'fp-geo-content',
'fp_geo_map_settings'
);
add_settings_field(
'cluster_enabled',
__('Agrupar marcadores', 'fp-geo-content'),
[__CLASS__, 'field_cluster'],
'fp-geo-content',
'fp_geo_map_settings'
);
add_settings_field(
'scroll_wheel_zoom',
__('Zoom con scroll', 'fp-geo-content'),
[__CLASS__, 'field_scroll_wheel_zoom'],
'fp-geo-content',
'fp_geo_map_settings'
);
// Sección: Marcadores
add_settings_section(
'fp_geo_markers',
__('Marcadores', 'fp-geo-content'),
[__CLASS__, 'section_markers'],
'fp-geo-content'
);
add_settings_field(
'marker_icon',
__('Icono personalizado', 'fp-geo-content'),
[__CLASS__, 'field_marker_icon'],
'fp-geo-content',
'fp_geo_markers'
);
add_settings_field(
'marker_default_color',
__('Color por defecto', 'fp-geo-content'),
[__CLASS__, 'field_marker_default_color'],
'fp-geo-content',
'fp_geo_markers'
);
add_settings_field(
'use_category_colors',
__('Colores por categoría', 'fp-geo-content'),
[__CLASS__, 'field_use_category_colors'],
'fp-geo-content',
'fp_geo_markers'
);
add_settings_field(
'show_legend',
__('Mostrar leyenda', 'fp-geo-content'),
[__CLASS__, 'field_show_legend'],
'fp-geo-content',
'fp_geo_markers'
);
// Sección: Filtros
add_settings_section(
'fp_geo_filters',
__('Filtros', 'fp-geo-content'),
[__CLASS__, 'section_filters'],
'fp-geo-content'
);
add_settings_field(
'filter_taxonomies',
__('Taxonomías para filtrar', 'fp-geo-content'),
[__CLASS__, 'field_filter_taxonomies'],
'fp-geo-content',
'fp_geo_filters'
);
add_settings_field(
'filter_combine',
__('Combinación de filtros', 'fp-geo-content'),
[__CLASS__, 'field_filter_combine'],
'fp-geo-content',
'fp_geo_filters'
);
// Sección: Visualización
add_settings_section(
'fp_geo_display',
__('Visualización', 'fp-geo-content'),
[__CLASS__, 'section_display'],
'fp-geo-content'
);
add_settings_field(
'detail_display',
__('Mostrar detalle en', 'fp-geo-content'),
[__CLASS__, 'field_detail_display'],
'fp-geo-content',
'fp_geo_display'
);
add_settings_field(
'sidebar_position',
__('Posición del panel lateral', 'fp-geo-content'),
[__CLASS__, 'field_sidebar_position'],
'fp-geo-content',
'fp_geo_display'
);
add_settings_field(
'show_detail_button',
__('Botón "Ver detalle"', 'fp-geo-content'),
[__CLASS__, 'field_show_detail_button'],
'fp-geo-content',
'fp_geo_display'
);
add_settings_field(
'detail_button_text',
__('Texto del botón', 'fp-geo-content'),
[__CLASS__, 'field_detail_button_text'],
'fp-geo-content',
'fp_geo_display'
);
// Sección: CSS Personalizado
add_settings_section(
'fp_geo_custom_css',
__('CSS Personalizado', 'fp-geo-content'),
[__CLASS__, 'section_custom_css'],
'fp-geo-content'
);
add_settings_field(
'custom_css',
__('Estilos CSS', 'fp-geo-content'),
[__CLASS__, 'field_custom_css'],
'fp-geo-content',
'fp_geo_custom_css'
);
}
/**
* Sanitizar opciones
*/
public static function sanitize_options($input) {
$sanitized = [];
// Post types
$sanitized['post_types'] = isset($input['post_types']) && is_array($input['post_types'])
? array_map('sanitize_text_field', $input['post_types'])
: [];
// Campos de lat/lng
$sanitized['lat_field'] = sanitize_text_field($input['lat_field'] ?? 'latitud');
$sanitized['lng_field'] = sanitize_text_field($input['lng_field'] ?? 'longitud');
// Centro por defecto
$sanitized['default_lat'] = floatval($input['default_lat'] ?? 40.4168);
$sanitized['default_lng'] = floatval($input['default_lng'] ?? -3.7038);
// Zoom
$sanitized['default_zoom'] = intval($input['default_zoom'] ?? 12);
$sanitized['min_zoom'] = intval($input['min_zoom'] ?? 5);
$sanitized['max_zoom'] = intval($input['max_zoom'] ?? 18);
// Cluster
$sanitized['cluster_enabled'] = isset($input['cluster_enabled']);
// Tile provider
$sanitized['tile_provider'] = sanitize_text_field($input['tile_provider'] ?? 'carto_light');
// Filtros
$sanitized['filter_taxonomies'] = isset($input['filter_taxonomies']) && is_array($input['filter_taxonomies'])
? array_map('sanitize_text_field', $input['filter_taxonomies'])
: [];
$sanitized['filter_combine'] = in_array($input['filter_combine'] ?? 'OR', ['OR', 'AND'])
? $input['filter_combine']
: 'OR';
// Display
$sanitized['detail_display'] = in_array($input['detail_display'] ?? 'sidebar', ['sidebar', 'modal'])
? $input['detail_display']
: 'sidebar';
// Scroll wheel zoom
$sanitized['scroll_wheel_zoom'] = sanitize_text_field($input['scroll_wheel_zoom'] ?? 'ctrl');
// Marcadores
$sanitized['marker_icon'] = isset($input['marker_icon']) ? absint($input['marker_icon']) : 0;
$sanitized['marker_default_color'] = sanitize_hex_color($input['marker_default_color'] ?? '#F97316');
$sanitized['use_category_colors'] = isset($input['use_category_colors']);
$sanitized['show_legend'] = isset($input['show_legend']);
$sanitized['legend_taxonomy'] = sanitize_text_field($input['legend_taxonomy'] ?? '');
// Sidebar position
$sanitized['sidebar_position'] = in_array($input['sidebar_position'] ?? 'right', ['left', 'right'])
? $input['sidebar_position']
: 'right';
// Detail button
$sanitized['show_detail_button'] = isset($input['show_detail_button']);
$sanitized['detail_button_text'] = sanitize_text_field($input['detail_button_text'] ?? __('Ver detalle', 'fp-geo-content'));
// Custom CSS - sanitizar pero permitir CSS válido
$sanitized['custom_css'] = isset($input['custom_css']) ? wp_strip_all_tags($input['custom_css']) : '';
return $sanitized;
}
/**
* Sección: Tipos de contenido
*/
public static function section_content_types() {
echo '<p>' . __('Selecciona los tipos de contenido que tienen campos de geolocalización y que quieres mostrar en el mapa.', 'fp-geo-content') . '</p>';
}
/**
* Campo: Post types
*/
public static function field_post_types() {
$options = get_option('fp_geo_content_options', []);
$selected = $options['post_types'] ?? [];
// Obtener todos los post types públicos
$post_types = get_post_types(['public' => true], 'objects');
echo '<fieldset>';
foreach ($post_types as $pt) {
if ($pt->name === 'attachment') continue;
$checked = in_array($pt->name, $selected) ? 'checked' : '';
printf(
'<label style="display:block;margin-bottom:8px;"><input type="checkbox" name="fp_geo_content_options[post_types][]" value="%s" %s> %s <code>(%s)</code></label>',
esc_attr($pt->name),
$checked,
esc_html($pt->label),
esc_html($pt->name)
);
}
echo '</fieldset>';
echo '<p class="description">' . __('Solo se mostrarán los posts que tengan coordenadas de latitud y longitud.', 'fp-geo-content') . '</p>';
}
/**
* Campo: Latitud
*/
public static function field_lat() {
$options = get_option('fp_geo_content_options', []);
$value = $options['lat_field'] ?? 'latitud';
printf(
'<input type="text" name="fp_geo_content_options[lat_field]" value="%s" class="regular-text">',
esc_attr($value)
);
echo '<p class="description">' . __('Nombre del campo ACF o meta que contiene la latitud.', 'fp-geo-content') . '</p>';
}
/**
* Campo: Longitud
*/
public static function field_lng() {
$options = get_option('fp_geo_content_options', []);
$value = $options['lng_field'] ?? 'longitud';
printf(
'<input type="text" name="fp_geo_content_options[lng_field]" value="%s" class="regular-text">',
esc_attr($value)
);
echo '<p class="description">' . __('Nombre del campo ACF o meta que contiene la longitud.', 'fp-geo-content') . '</p>';
}
/**
* Sección: Configuración del mapa
*/
public static function section_map_settings() {
echo '<p>' . __('Configura el comportamiento y apariencia del mapa.', 'fp-geo-content') . '</p>';
}
/**
* Campo: Centro por defecto
*/
public static function field_default_center() {
$options = get_option('fp_geo_content_options', []);
$lat = $options['default_lat'] ?? '40.4168';
$lng = $options['default_lng'] ?? '-3.7038';
printf(
'<label>Latitud: <input type="text" name="fp_geo_content_options[default_lat]" value="%s" style="width:120px;"></label> ',
esc_attr($lat)
);
printf(
'<label>Longitud: <input type="text" name="fp_geo_content_options[default_lng]" value="%s" style="width:120px;"></label>',
esc_attr($lng)
);
echo '<p class="description">' . __('Coordenadas del centro inicial del mapa (por defecto: Madrid).', 'fp-geo-content') . '</p>';
}
/**
* Campo: Niveles de zoom
*/
public static function field_zoom_levels() {
$options = get_option('fp_geo_content_options', []);
$default = $options['default_zoom'] ?? 12;
$min = $options['min_zoom'] ?? 5;
$max = $options['max_zoom'] ?? 18;
printf(
'<label>Por defecto: <input type="number" name="fp_geo_content_options[default_zoom]" value="%s" min="1" max="20" style="width:60px;"></label> ',
esc_attr($default)
);
printf(
'<label>Mínimo: <input type="number" name="fp_geo_content_options[min_zoom]" value="%s" min="1" max="20" style="width:60px;"></label> ',
esc_attr($min)
);
printf(
'<label>Máximo: <input type="number" name="fp_geo_content_options[max_zoom]" value="%s" min="1" max="20" style="width:60px;"></label>',
esc_attr($max)
);
}
/**
* Campo: Proveedor de tiles
*/
public static function field_tile_provider() {
$options = get_option('fp_geo_content_options', []);
$value = $options['tile_provider'] ?? 'carto_light';
$providers = [
'osm' => 'OpenStreetMap (estándar)',
'carto_light' => 'CartoDB Positron (claro)',
'carto_dark' => 'CartoDB Dark Matter (oscuro)',
'carto_voyager' => 'CartoDB Voyager (colores)',
'stamen_toner' => 'Stamen Toner (B/N)',
'stamen_watercolor' => 'Stamen Watercolor (acuarela)',
];
echo '<select name="fp_geo_content_options[tile_provider]">';
foreach ($providers as $key => $label) {
printf(
'<option value="%s" %s>%s</option>',
esc_attr($key),
selected($value, $key, false),
esc_html($label)
);
}
echo '</select>';
}
/**
* Campo: Cluster
*/
public static function field_cluster() {
$options = get_option('fp_geo_content_options', []);
$checked = isset($options['cluster_enabled']) && $options['cluster_enabled'] ? 'checked' : '';
printf(
'<label><input type="checkbox" name="fp_geo_content_options[cluster_enabled]" value="1" %s> %s</label>',
$checked,
__('Agrupar marcadores cercanos en clusters', 'fp-geo-content')
);
}
/**
* Sección: Filtros
*/
public static function section_filters() {
echo '<p>' . __('Configura qué taxonomías se pueden usar como filtros en el mapa.', 'fp-geo-content') . '</p>';
}
/**
* Campo: Taxonomías para filtrar
*/
public static function field_filter_taxonomies() {
$options = get_option('fp_geo_content_options', []);
$selected = $options['filter_taxonomies'] ?? [];
// Obtener todas las taxonomías públicas
$taxonomies = get_taxonomies(['public' => true], 'objects');
echo '<fieldset>';
foreach ($taxonomies as $tax) {
if (in_array($tax->name, ['post_format'])) continue;
$checked = in_array($tax->name, $selected) ? 'checked' : '';
printf(
'<label style="display:block;margin-bottom:8px;"><input type="checkbox" name="fp_geo_content_options[filter_taxonomies][]" value="%s" %s> %s <code>(%s)</code></label>',
esc_attr($tax->name),
$checked,
esc_html($tax->label),
esc_html($tax->name)
);
}
echo '</fieldset>';
}
/**
* Campo: Combinación de filtros
*/
public static function field_filter_combine() {
$options = get_option('fp_geo_content_options', []);
$value = $options['filter_combine'] ?? 'OR';
echo '<select name="fp_geo_content_options[filter_combine]">';
printf('<option value="OR" %s>%s</option>', selected($value, 'OR', false), __('OR - Mostrar si cumple cualquier filtro', 'fp-geo-content'));
printf('<option value="AND" %s>%s</option>', selected($value, 'AND', false), __('AND - Mostrar solo si cumple todos los filtros', 'fp-geo-content'));
echo '</select>';
}
/**
* Sección: Visualización
*/
public static function section_display() {
echo '<p>' . __('Configura cómo se muestra la información de cada marcador.', 'fp-geo-content') . '</p>';
}
/**
* Campo: Mostrar detalle
*/
public static function field_detail_display() {
$options = get_option('fp_geo_content_options', []);
$value = $options['detail_display'] ?? 'sidebar';
echo '<select name="fp_geo_content_options[detail_display]">';
printf('<option value="sidebar" %s>%s</option>', selected($value, 'sidebar', false), __('Panel lateral sobre el mapa', 'fp-geo-content'));
printf('<option value="modal" %s>%s</option>', selected($value, 'modal', false), __('Ventana modal/popup', 'fp-geo-content'));
echo '</select>';
echo '<p class="description">' . __('Cómo mostrar la ficha de detalle al hacer clic en un marcador.', 'fp-geo-content') . '</p>';
}
/**
* Campo: Scroll wheel zoom
*/
public static function field_scroll_wheel_zoom() {
$options = get_option('fp_geo_content_options', []);
$value = $options['scroll_wheel_zoom'] ?? 'ctrl';
echo '<select name="fp_geo_content_options[scroll_wheel_zoom]">';
printf('<option value="ctrl" %s>%s</option>', selected($value, 'ctrl', false), __('Solo con Ctrl + scroll (recomendado)', 'fp-geo-content'));
printf('<option value="always" %s>%s</option>', selected($value, 'always', false), __('Siempre activo', 'fp-geo-content'));
printf('<option value="disabled" %s>%s</option>', selected($value, 'disabled', false), __('Desactivado', 'fp-geo-content'));
echo '</select>';
echo '<p class="description">' . __('Controla si el scroll del ratón hace zoom en el mapa. "Solo con Ctrl" permite hacer scroll en la página sin hacer zoom accidentalmente.', 'fp-geo-content') . '</p>';
}
/**
* Sección: Marcadores
*/
public static function section_markers() {
echo '<p>' . __('Personaliza la apariencia de los marcadores en el mapa.', 'fp-geo-content') . '</p>';
}
/**
* Campo: Icono personalizado
*/
public static function field_marker_icon() {
$options = get_option('fp_geo_content_options', []);
$image_id = $options['marker_icon'] ?? 0;
$image_url = $image_id ? wp_get_attachment_image_url($image_id, 'thumbnail') : '';
echo '<div class="fp-geo-media-upload">';
printf(
'<input type="hidden" name="fp_geo_content_options[marker_icon]" id="fp_geo_marker_icon" value="%s">',
esc_attr($image_id)
);
printf(
'<div id="fp_geo_marker_icon_preview" class="fp-geo-image-preview" style="%s">',
$image_url ? '' : 'display:none;'
);
if ($image_url) {
printf('<img src="%s" alt="" style="max-width: 60px; height: auto;">', esc_url($image_url));
}
echo '</div>';
printf(
'<button type="button" class="button fp-geo-upload-btn" data-target="fp_geo_marker_icon">%s</button>',
__('Subir icono', 'fp-geo-content')
);
printf(
'<button type="button" class="button fp-geo-remove-btn" data-target="fp_geo_marker_icon" style="%s">%s</button>',
$image_id ? '' : 'display:none;',
__('Eliminar', 'fp-geo-content')
);
echo '</div>';
echo '<p class="description">' . __('Opcional: Sube un icono personalizado para los marcadores. Si no se sube, se usarán círculos de color. Tamaño recomendado: 40x50px.', 'fp-geo-content') . '</p>';
}
/**
* Campo: Color por defecto
*/
public static function field_marker_default_color() {
$options = get_option('fp_geo_content_options', []);
$value = $options['marker_default_color'] ?? '#F97316';
printf(
'<input type="color" name="fp_geo_content_options[marker_default_color]" value="%s" class="fp-geo-color-picker">',
esc_attr($value)
);
echo '<p class="description">' . __('Color por defecto para los marcadores cuando no tienen color de categoría asignado.', 'fp-geo-content') . '</p>';
}
/**
* Campo: Usar colores por categoría
*/
public static function field_use_category_colors() {
$options = get_option('fp_geo_content_options', []);
$checked = isset($options['use_category_colors']) && $options['use_category_colors'] ? 'checked' : '';
$legend_taxonomy = $options['legend_taxonomy'] ?? '';
printf(
'<label><input type="checkbox" name="fp_geo_content_options[use_category_colors]" value="1" %s> %s</label>',
$checked,
__('Usar colores de las categorías de los filtros (si tienen campo ACF "color")', 'fp-geo-content')
);
// Selector de taxonomía para los colores
$taxonomies = get_taxonomies(['public' => true], 'objects');
echo '<div class="fp-geo-category-color-options" style="margin-top: 10px; margin-left: 25px;">';
echo '<label>' . __('Taxonomía para colores:', 'fp-geo-content') . ' ';
echo '<select name="fp_geo_content_options[legend_taxonomy]">';
echo '<option value="">' . __('-- Seleccionar --', 'fp-geo-content') . '</option>';
foreach ($taxonomies as $tax) {
if (in_array($tax->name, ['post_format'])) continue;
printf(
'<option value="%s" %s>%s</option>',
esc_attr($tax->name),
selected($legend_taxonomy, $tax->name, false),
esc_html($tax->label)
);
}
echo '</select></label>';
echo '<p class="description">' . __('Selecciona la taxonomía cuyos colores quieres usar para los marcadores.', 'fp-geo-content') . '</p>';
echo '</div>';
}
/**
* Campo: Mostrar leyenda
*/
public static function field_show_legend() {
$options = get_option('fp_geo_content_options', []);
$checked = isset($options['show_legend']) && $options['show_legend'] ? 'checked' : '';
printf(
'<label><input type="checkbox" name="fp_geo_content_options[show_legend]" value="1" %s> %s</label>',
$checked,
__('Mostrar leyenda de colores sobre el mapa', 'fp-geo-content')
);
echo '<p class="description">' . __('Muestra una leyenda con los colores de las categorías en la esquina del mapa.', 'fp-geo-content') . '</p>';
}
/**
* Campo: Posición del sidebar
*/
public static function field_sidebar_position() {
$options = get_option('fp_geo_content_options', []);
$value = $options['sidebar_position'] ?? 'right';
echo '<select name="fp_geo_content_options[sidebar_position]">';
printf('<option value="right" %s>%s</option>', selected($value, 'right', false), __('Derecha', 'fp-geo-content'));
printf('<option value="left" %s>%s</option>', selected($value, 'left', false), __('Izquierda', 'fp-geo-content'));
echo '</select>';
echo '<p class="description">' . __('Posición del panel lateral cuando se muestra el detalle (solo aplica al modo sidebar).', 'fp-geo-content') . '</p>';
}
/**
* Campo: Mostrar botón detalle
*/
public static function field_show_detail_button() {
$options = get_option('fp_geo_content_options', []);
// Por defecto activado si no existe la opción
$checked = !isset($options['show_detail_button']) || $options['show_detail_button'] ? 'checked' : '';
printf(
'<label><input type="checkbox" name="fp_geo_content_options[show_detail_button]" value="1" %s> %s</label>',
$checked,
__('Mostrar botón para ir al detalle del contenido', 'fp-geo-content')
);
}
/**
* Campo: Texto del botón
*/
public static function field_detail_button_text() {
$options = get_option('fp_geo_content_options', []);
$value = $options['detail_button_text'] ?? __('Ver detalle', 'fp-geo-content');
printf(
'<input type="text" name="fp_geo_content_options[detail_button_text]" value="%s" class="regular-text">',
esc_attr($value)
);
echo '<p class="description">' . __('Texto del botón que enlaza al contenido completo. Ejemplos: "Ver detalle", "Ver más", "Ir al contenido"...', 'fp-geo-content') . '</p>';
}
/**
* Sección: CSS Personalizado
*/
public static function section_custom_css() {
echo '<p>' . __('Añade CSS personalizado para sobreescribir los estilos base del mapa. Estos estilos se cargarán después de los estilos base del plugin.', 'fp-geo-content') . '</p>';
}
/**
* Campo: CSS Personalizado
*/
public static function field_custom_css() {
$options = get_option('fp_geo_content_options', []);
$value = $options['custom_css'] ?? '';
printf(
'<textarea name="fp_geo_content_options[custom_css]" rows="20" class="large-text code" style="font-family: monospace; font-size: 13px;">%s</textarea>',
esc_textarea($value)
);
echo '<p class="description">' . __('Introduce CSS válido para personalizar el aspecto del mapa. No incluyas etiquetas &lt;style&gt;.', 'fp-geo-content') . '</p>';
// Mostrar clases disponibles
echo '<details style="margin-top: 15px;">';
echo '<summary style="cursor: pointer; font-weight: 500;">' . __('Ver clases CSS disponibles', 'fp-geo-content') . '</summary>';
echo '<div style="background: #f5f5f5; padding: 15px; margin-top: 10px; border-radius: 4px; font-size: 13px;">';
echo '<code>.fp-geo-wrapper</code> - Contenedor principal<br>';
echo '<code>.fp-geo-filters</code> - Barra de filtros<br>';
echo '<code>.fp-geo-filter-group</code> - Grupo de filtros<br>';
echo '<code>.fp-geo-filter-label</code> - Etiqueta del grupo<br>';
echo '<code>.fp-geo-filter-btn</code> - Botón de filtro<br>';
echo '<code>.fp-geo-filter-btn.active</code> - Botón activo<br>';
echo '<code>.fp-geo-clear-filters</code> - Botón limpiar<br>';
echo '<code>.fp-geo-map</code> - Contenedor del mapa<br>';
echo '<code>.fp-geo-legend</code> - Leyenda<br>';
echo '<code>.fp-geo-legend-item</code> - Item de leyenda<br>';
echo '<code>.fp-geo-legend-marker</code> - Marcador en leyenda<br>';
echo '<code>.fp-geo-detail</code> - Panel de detalle<br>';
echo '<code>.fp-geo-sidebar</code> - Panel lateral<br>';
echo '<code>.fp-geo-modal</code> - Modal<br>';
echo '<code>.fp-geo-detail-header</code> - Cabecera del detalle<br>';
echo '<code>.fp-geo-detail-title</code> - Título<br>';
echo '<code>.fp-geo-detail-excerpt</code> - Extracto<br>';
echo '<code>.fp-geo-btn</code> - Botones<br>';
echo '<code>.fp-geo-circle-marker</code> - Marcadores circulares<br>';
echo '<code>.marker-cluster</code> - Clusters<br>';
echo '</div>';
echo '</details>';
}
/**
* Obtener proveedores de tiles
*/
public static function get_tile_providers() {
return [
'osm' => [
'url' => 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
'attribution' => '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
'subdomains' => 'abc',
],
'carto_light' => [
'url' => 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
'attribution' => '&copy; <a href="https://carto.com/">CARTO</a>',
'subdomains' => 'abcd',
],
'carto_dark' => [
'url' => 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
'attribution' => '&copy; <a href="https://carto.com/">CARTO</a>',
'subdomains' => 'abcd',
],
'carto_voyager' => [
'url' => 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
'attribution' => '&copy; <a href="https://carto.com/">CARTO</a>',
'subdomains' => 'abcd',
],
'stamen_toner' => [
'url' => 'https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}{r}.png',
'attribution' => '&copy; <a href="https://stadiamaps.com/">Stadia Maps</a>',
'subdomains' => '',
],
'stamen_watercolor' => [
'url' => 'https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg',
'attribution' => '&copy; <a href="https://stadiamaps.com/">Stadia Maps</a>',
'subdomains' => '',
],
];
}
}