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

182
fp-geo-content/README.md Normal file
View File

@@ -0,0 +1,182 @@
# FP Geo Content
Plugin genérico de WordPress para mostrar contenido geolocalizado en un mapa interactivo usando Leaflet y OpenStreetMap.
## Descripción
FP Geo Content permite mostrar cualquier tipo de contenido de WordPress que tenga campos de latitud y longitud en un mapa interactivo. Es completamente configurable y reutilizable en diferentes proyectos.
## Características
- 🗺️ **Mapa interactivo** con Leaflet y OpenStreetMap (sin necesidad de API keys)
- 📍 **Marcadores personalizables** con iconos y colores por categoría
- 🔍 **Filtros dinámicos** por taxonomías
- 📱 **Responsive** - funciona en móviles y tablets
- 🎨 **Personalizable** - variables CSS para adaptar al diseño
- 🔌 **Genérico** - funciona con cualquier CPT que tenga coordenadas
## Instalación
1. Sube la carpeta `fp-geo-content` a `/wp-content/plugins/`
2. Activa el plugin desde el panel de WordPress
3. Ve a **Ajustes > FP Geo Content** para configurar
## Configuración
### 1. Tipos de Contenido
Selecciona qué tipos de contenido (posts, páginas, CPTs) quieres mostrar en el mapa.
### 2. Campos de Geolocalización
Indica el nombre de los campos ACF o meta que contienen la latitud y longitud:
- Campo de latitud (por defecto: `latitud`)
- Campo de longitud (por defecto: `longitud`)
### 3. Configuración del Mapa
- **Centro por defecto**: Coordenadas iniciales del mapa
- **Niveles de zoom**: Mínimo, máximo y por defecto
- **Proveedor de tiles**: OpenStreetMap, CartoDB, Stamen, etc.
- **Clusters**: Agrupar marcadores cercanos
### 4. Filtros
- Selecciona qué taxonomías pueden usarse como filtros
- Configura si los filtros se combinan con OR o AND
### 5. Visualización
- **Sidebar**: Panel lateral que se desliza sobre el mapa
- **Modal**: Ventana emergente centrada
## Uso
### Shortcode básico
```
[fp-geo-map]
```
### Con atributos personalizados
```
[fp-geo-map post_types="actuacion,entidad" height="700px" filters="true"]
```
### Todos los atributos
| Atributo | Descripción | Ejemplo |
|----------|-------------|---------|
| `post_types` | Tipos de contenido (separados por coma) | `post_types="actuacion,entidad"` |
| `taxonomies` | Taxonomías para filtros | `taxonomies="iniciativa,linea_trabajo"` |
| `height` | Altura del mapa | `height="500px"` |
| `lat` / `lng` | Centro inicial | `lat="40.4168" lng="-3.7038"` |
| `zoom` | Nivel de zoom (1-18) | `zoom="14"` |
| `filters` | Mostrar filtros | `filters="true"` |
| `detail` | Modo de detalle | `detail="sidebar"` o `detail="modal"` |
| `cluster` | Agrupar marcadores | `cluster="true"` |
| `class` | Clases CSS adicionales | `class="mi-mapa"` |
## Personalización de estilos
### Variables CSS
```css
:root {
--fp-geo-primary: #1E6B52;
--fp-geo-secondary: #2A9D8F;
--fp-geo-accent: #F4A261;
--fp-geo-text: #333;
--fp-geo-bg: #fff;
--fp-geo-radius: 8px;
}
```
### Selectores CSS principales
| Selector | Descripción |
|----------|-------------|
| `.fp-geo-wrapper` | Contenedor principal |
| `.fp-geo-map` | Elemento del mapa |
| `.fp-geo-filters` | Barra de filtros |
| `.fp-geo-filter-btn` | Botones de filtro |
| `.fp-geo-detail` | Panel de detalle |
| `.fp-geo-sidebar` | Panel lateral |
| `.fp-geo-modal` | Ventana modal |
### Ejemplo de personalización
```css
/* Cambiar colores del tema */
.fp-geo-filters {
background: #your-color;
}
.fp-geo-filter-btn.active {
background: #your-accent;
}
/* Personalizar panel de detalle */
.fp-geo-detail-header {
background: linear-gradient(135deg, #color1, #color2);
}
```
## Hooks y Filtros
### Filtrar datos del marcador
```php
add_filter('fp_geo_marker_data', function($marker, $post) {
// Añadir campos personalizados
$marker['custom_field'] = get_field('mi_campo', $post->ID);
return $marker;
}, 10, 2);
```
## API JavaScript
```javascript
// Obtener instancia del mapa
const mapData = FPGeoContent.getMap('fp-geo-map-1');
// Acceder al mapa Leaflet
const leafletMap = mapData.map;
// Refrescar marcadores
FPGeoContent.refreshMarkers('fp-geo-map-1', newMarkers);
// Aplicar filtros programáticamente
FPGeoContent.applyFilters('fp-geo-map-1');
```
## Proveedores de Tiles disponibles
- **OpenStreetMap** (estándar)
- **CartoDB Positron** (claro)
- **CartoDB Dark Matter** (oscuro)
- **CartoDB Voyager** (colores)
- **Stamen Toner** (blanco y negro)
- **Stamen Watercolor** (acuarela)
## Requisitos
- WordPress 6.0+
- PHP 8.0+
- ACF (recomendado para campos de geolocalización)
## Compatibilidad
- ✅ Divi Theme Builder
- ✅ Elementor
- ✅ Gutenberg
- ✅ Classic Editor
## Soporte
Plugin desarrollado por [Freepress Coop](https://freepress.coop)
## Licencia
GPL-2.0+

View File

@@ -0,0 +1,132 @@
/**
* Estilos del admin - FP Geo Content
*/
.fp-geo-settings {
max-width: 1000px;
}
.fp-geo-settings h1 {
display: flex;
align-items: center;
gap: 10px;
color: #1E6B52;
}
.fp-geo-settings h1 .dashicons {
font-size: 28px;
width: 28px;
height: 28px;
}
.fp-geo-settings-intro {
background: linear-gradient(135deg, #1E6B52 0%, #2A9D8F 100%);
color: white;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.fp-geo-settings-intro p {
margin: 0;
font-size: 15px;
}
.fp-geo-settings h2 {
color: #1E6B52;
border-bottom: 2px solid #1E6B52;
padding-bottom: 10px;
margin-top: 30px;
}
.fp-geo-settings h3 {
color: #333;
margin-top: 25px;
}
.fp-geo-settings-help {
background: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-top: 30px;
}
.fp-geo-shortcode {
display: block;
background: #f0f0f0;
padding: 15px 20px;
border-radius: 6px;
font-size: 16px;
margin: 15px 0;
}
.fp-geo-settings pre {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
overflow-x: auto;
}
.fp-geo-settings pre code {
background: none;
padding: 0;
}
.fp-geo-settings table code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
/* Media uploader */
.fp-geo-media-upload {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.fp-geo-image-preview {
border: 2px dashed #ccc;
border-radius: 6px;
padding: 10px;
background: #f9f9f9;
}
.fp-geo-image-preview img {
display: block;
border-radius: 4px;
}
.fp-geo-upload-btn,
.fp-geo-remove-btn {
margin-top: 0 !important;
}
.fp-geo-remove-btn {
color: #dc3232;
}
/* Color picker */
.fp-geo-color-picker {
width: 60px;
height: 40px;
padding: 2px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
}
/* Category color options */
.fp-geo-category-color-options {
background: #f9f9f9;
padding: 15px;
border-radius: 6px;
border: 1px solid #e0e0e0;
}
.fp-geo-category-color-options select {
margin-left: 5px;
}

View File

@@ -0,0 +1,514 @@
/**
* Estilos base del mapa - FP Geo Content
*
* Estilos mínimos para layout y funcionalidad.
* Los estilos visuales se pueden personalizar desde:
* - Ajustes > FP Geo Content > CSS Personalizado
* - O desde el tema activo
*/
/* === LAYOUT BASE === */
.fp-geo-wrapper {
position: relative;
width: 100%;
font-family: inherit;
}
.fp-geo-map-container {
position: relative;
display: flex;
width: 100%;
}
.fp-geo-map {
flex: 1;
min-height: 400px;
z-index: 1;
}
/* === FILTROS - LAYOUT === */
.fp-geo-filters {
padding: 1rem;
}
.fp-geo-filters-inner {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: flex-start;
}
.fp-geo-filter-group {
flex: 1;
min-width: 200px;
}
.fp-geo-filter-label {
display: block;
font-size: 0.875rem;
margin-bottom: 0.5rem;
font-weight: 500;
}
.fp-geo-filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.fp-geo-filter-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border: 1px solid currentColor;
border-radius: 4px;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
background: transparent;
}
.fp-geo-filter-icon {
width: 18px;
height: 18px;
}
.fp-geo-clear-filters {
padding: 8px 16px;
border-radius: 4px;
font-size: 0.8rem;
cursor: pointer;
background: transparent;
font-weight: bold;
}
/* Footer de filtros con contador */
.fp-geo-filters-footer {
display: flex;
align-items: center;
gap: 16px;
margin-top: 8px;
flex-wrap: wrap;
}
.fp-geo-results-count {
font-size: 0.875rem;
}
.fp-geo-results-number {
font-weight: 600;
}
/* === LEYENDA - LAYOUT === */
.fp-geo-legend {
position: absolute;
bottom: 20px;
right: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
padding: 14px 18px;
z-index: 800;
min-width: 180px;
}
.fp-geo-sidebar-left .fp-geo-legend {
right: auto;
left: 20px;
}
.fp-geo-legend-title {
font-size: 0.85rem;
font-weight: 700;
margin-bottom: 10px;
letter-spacing: 0.01em;
}
/* Secciones */
.fp-geo-legend-section {
margin-bottom: 4px;
}
.fp-geo-legend-section-title {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #6b7280;
margin-bottom: 6px;
}
.fp-geo-legend-items {
display: flex;
flex-direction: column;
gap: 7px;
}
.fp-geo-legend-item {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.85rem;
line-height: 1.3;
}
/* Marcador circular (iniciativas) */
.fp-geo-legend-marker {
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
flex-shrink: 0;
}
/* Sección Bloque Piloto — separada visualmente */
.fp-geo-legend-pilot-section {
margin-top: 8px;
padding-top: 10px;
border-top: 1px solid #e5e7eb;
}
.fp-geo-legend-marker-pilot {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 28px;
}
.fp-geo-legend-marker-pilot svg {
display: block;
filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2));
}
.fp-geo-legend-pilot-desc {
font-size: 0.7rem;
color: #9ca3af;
margin: 2px 0 0 0;
line-height: 1.35;
}
/* === AVISO DE SCROLL === */
.fp-geo-scroll-hint {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.75);
color: white;
padding: 12px 24px;
border-radius: 25px;
font-size: 0.9rem;
z-index: 900;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
white-space: nowrap;
}
.fp-geo-scroll-hint.visible {
opacity: 1;
}
/* === PANEL DE DETALLE - LAYOUT === */
.fp-geo-detail {
position: absolute;
background: #fff;
z-index: 1000;
overflow: hidden;
transition: transform 0.3s ease, opacity 0.3s ease;
}
/* Sidebar derecha (por defecto) */
.fp-geo-sidebar {
top: 0;
right: 0;
width: 350px;
max-width: 40%;
height: 100%;
box-shadow: -4px 0 20px rgba(0,0,0,0.15);
transform: translateX(100%);
opacity: 0;
pointer-events: none;
}
.fp-geo-sidebar.visible {
transform: translateX(0);
opacity: 1;
pointer-events: auto;
}
/* Sidebar izquierda */
.fp-geo-sidebar-left .fp-geo-sidebar {
right: auto;
left: 0;
transform: translateX(-100%);
box-shadow: 4px 0 20px rgba(0,0,0,0.15);
}
.fp-geo-sidebar-left .fp-geo-sidebar.visible {
transform: translateX(0);
}
/* Modal */
.fp-geo-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.9);
width: 90%;
max-width: 500px;
max-height: 80vh;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
opacity: 0;
pointer-events: none;
z-index: 10001;
}
.fp-geo-modal.visible {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
pointer-events: auto;
}
.fp-geo-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.fp-geo-overlay.visible {
opacity: 1;
pointer-events: auto;
}
/* === CONTENIDO DEL DETALLE === */
.fp-geo-detail-content {
height: 100%;
overflow-y: auto;
}
.fp-geo-detail-close {
position: absolute;
top: 10px;
right: 10px;
width: 32px;
height: 32px;
background: rgba(255,255,255,0.9);
border: none;
border-radius: 50%;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
}
.fp-geo-detail-header {
padding: 1.5rem;
}
.fp-geo-detail-taxonomies {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.fp-geo-detail-tax-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.75rem;
}
.fp-geo-detail-tax-icon {
width: 14px;
height: 14px;
}
.fp-geo-detail-thumbnail {
width: 100%;
aspect-ratio: 16/9;
overflow: hidden;
display: none;
}
.fp-geo-detail-thumbnail.has-image {
display: block;
}
.fp-geo-detail-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.fp-geo-detail-title {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
padding: 1.25rem 1.25rem 0.5rem;
line-height: 1.3;
}
.fp-geo-detail-excerpt {
font-size: 0.9rem;
line-height: 1.5;
padding: 0 1.25rem;
margin-bottom: 1rem;
}
.fp-geo-detail-meta {
padding: 0 1.25rem;
margin-bottom: 1rem;
}
.fp-geo-detail-location,
.fp-geo-detail-contact {
font-size: 0.85rem;
}
.fp-geo-detail-location {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 0.75rem;
}
.fp-geo-detail-contact-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 0.5rem;
}
.fp-geo-detail-contact-item a {
text-decoration: none;
}
.fp-geo-detail-footer {
padding: 1.25rem;
border-top: 1px solid #eee;
}
.fp-geo-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 24px;
border: none;
border-radius: 4px;
font-size: 0.9rem;
font-weight: 500;
text-decoration: none;
cursor: pointer;
}
/* === MARCADORES === */
.fp-geo-circle-marker {
background: transparent !important;
border: none !important;
}
.fp-geo-circle-marker svg {
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
}
/* === CLUSTERS === */
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
}
.marker-cluster div {
width: 30px;
height: 30px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
font: 12px sans-serif;
font-weight: bold;
line-height: 30px;
}
/* === ERROR === */
.fp-geo-error {
background: #fee;
color: #c00;
padding: 15px 20px;
border-radius: 8px;
border-left: 4px solid #c00;
}
/* === RESPONSIVE === */
@media (max-width: 768px) {
.fp-geo-filters-inner {
flex-direction: column;
align-items: stretch;
}
.fp-geo-filter-group {
min-width: 100%;
}
.fp-geo-sidebar {
width: 100%;
max-width: 100%;
height: 60%;
top: auto;
bottom: 0;
right: 0;
left: 0;
transform: translateY(100%);
border-radius: 12px 12px 0 0;
box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
}
.fp-geo-sidebar.visible {
transform: translateY(0);
}
.fp-geo-sidebar-left .fp-geo-sidebar {
right: 0;
left: 0;
transform: translateY(100%);
}
.fp-geo-sidebar-left .fp-geo-sidebar.visible {
transform: translateY(0);
}
.fp-geo-modal {
width: 95%;
max-height: 90vh;
}
.fp-geo-legend {
bottom: 10px;
right: 10px;
left: auto;
padding: 10px 12px;
}
.fp-geo-sidebar-left .fp-geo-legend {
right: 10px;
left: auto;
}
}

View File

@@ -0,0 +1,88 @@
/**
* Scripts del admin - FP Geo Content
*/
(function($) {
'use strict';
$(document).ready(function() {
// Copiar shortcode al portapapeles
$('.fp-geo-shortcode').on('click', function() {
const text = $(this).text();
navigator.clipboard.writeText(text).then(function() {
alert('Shortcode copiado al portapapeles');
});
}).css('cursor', 'pointer').attr('title', 'Clic para copiar');
// Media uploader para icono personalizado
let mediaUploader;
$('.fp-geo-upload-btn').on('click', function(e) {
e.preventDefault();
const targetId = $(this).data('target');
const $input = $('#' + targetId);
const $preview = $('#' + targetId + '_preview');
const $removeBtn = $('.fp-geo-remove-btn[data-target="' + targetId + '"]');
// Si el uploader ya existe, abrirlo
if (mediaUploader) {
mediaUploader.open();
return;
}
// Crear el media uploader
mediaUploader = wp.media({
title: 'Seleccionar icono de marcador',
button: {
text: 'Usar este icono'
},
multiple: false,
library: {
type: 'image'
}
});
// Cuando se selecciona una imagen
mediaUploader.on('select', function() {
const attachment = mediaUploader.state().get('selection').first().toJSON();
// Actualizar input
$input.val(attachment.id);
// Mostrar preview
const imgUrl = attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url;
$preview.html('<img src="' + imgUrl + '" alt="" style="max-width: 60px; height: auto;">').show();
// Mostrar botón eliminar
$removeBtn.show();
});
mediaUploader.open();
});
// Eliminar imagen
$('.fp-geo-remove-btn').on('click', function(e) {
e.preventDefault();
const targetId = $(this).data('target');
const $input = $('#' + targetId);
const $preview = $('#' + targetId + '_preview');
$input.val('');
$preview.html('').hide();
$(this).hide();
});
// Toggle opciones de color por categoría
$('input[name="fp_geo_content_options[use_category_colors]"]').on('change', function() {
const $options = $('.fp-geo-category-color-options');
if ($(this).is(':checked')) {
$options.slideDown();
} else {
$options.slideUp();
}
}).trigger('change');
});
})(jQuery);

View File

@@ -0,0 +1,630 @@
/**
* Script del mapa - FP Geo Content
*/
(function() {
'use strict';
// Almacenar instancias de mapas
const mapInstances = {};
/**
* Inicializar cuando el DOM esté listo
*/
document.addEventListener('DOMContentLoaded', function() {
// Buscar todos los wrappers de mapas
document.querySelectorAll('.fp-geo-wrapper').forEach(function(wrapper) {
const instance = wrapper.dataset.instance;
const configName = 'fpGeoConfig_' + instance;
if (window[configName]) {
initMap(window[configName]);
}
});
});
/**
* Inicializar un mapa
*/
function initMap(config) {
const mapEl = document.getElementById(config.mapId);
if (!mapEl) return;
// Configurar opciones de scroll wheel zoom
let scrollWheelZoomOption = false;
if (config.scrollWheelZoom === 'always') {
scrollWheelZoomOption = true;
} else if (config.scrollWheelZoom === 'ctrl') {
scrollWheelZoomOption = 'ctrl';
}
// Crear mapa Leaflet
const map = L.map(mapEl, {
center: config.center,
zoom: config.zoom,
minZoom: config.minZoom,
maxZoom: config.maxZoom,
scrollWheelZoom: scrollWheelZoomOption === 'ctrl' ? false : scrollWheelZoomOption,
});
// Añadir capa de tiles
L.tileLayer(config.tileUrl, {
attribution: config.tileAttribution,
subdomains: config.tileSubdomains || 'abc',
maxZoom: 19,
}).addTo(map);
// Crear grupo de marcadores (con o sin cluster)
let markersLayer;
if (config.clusterEnabled) {
markersLayer = L.markerClusterGroup({
showCoverageOnHover: false,
maxClusterRadius: 50,
});
} else {
markersLayer = L.layerGroup();
}
// Almacenar datos
const mapData = {
map: map,
markersLayer: markersLayer,
allMarkers: [],
config: config,
activeFilters: {},
};
mapInstances[config.mapId] = mapData;
// Configurar control de scroll con Ctrl
if (config.scrollWheelZoom === 'ctrl') {
initCtrlScrollZoom(mapData);
}
// Añadir marcadores iniciales
addMarkers(mapData, config.markers);
// Añadir capa al mapa
map.addLayer(markersLayer);
// Ajustar zoom para mostrar todos los marcadores
fitMapToMarkers(mapData);
// Inicializar filtros
initFilters(mapData);
// Inicializar panel de detalle
initDetailPanel(mapData);
}
/**
* Inicializar control de zoom con Ctrl+scroll
*/
function initCtrlScrollZoom(mapData) {
const { map, config } = mapData;
const mapEl = document.getElementById(config.mapId);
let hintTimeout;
// Crear elemento de aviso
const scrollHint = document.createElement('div');
scrollHint.className = 'fp-geo-scroll-hint';
scrollHint.textContent = config.i18n.scrollZoomHint || 'Usa Ctrl + scroll para hacer zoom';
mapEl.parentElement.appendChild(scrollHint);
// Detectar scroll sin Ctrl
mapEl.addEventListener('wheel', function(e) {
if (e.ctrlKey) {
// Habilitar zoom cuando Ctrl está presionado
map.scrollWheelZoom.enable();
scrollHint.classList.remove('visible');
} else {
// Mostrar aviso
map.scrollWheelZoom.disable();
scrollHint.classList.add('visible');
clearTimeout(hintTimeout);
hintTimeout = setTimeout(function() {
scrollHint.classList.remove('visible');
}, 1500);
}
});
// Deshabilitar zoom al soltar Ctrl
document.addEventListener('keyup', function(e) {
if (e.key === 'Control') {
map.scrollWheelZoom.disable();
}
});
}
/**
* Ajustar el zoom del mapa para mostrar todos los marcadores
*/
function fitMapToMarkers(mapData) {
const { map, markersLayer, config } = mapData;
// Obtener los límites del grupo de marcadores
const layers = markersLayer.getLayers();
if (layers.length === 0) {
// Sin marcadores, usar centro y zoom por defecto
return;
}
if (layers.length === 1) {
// Un solo marcador: centrar en él con zoom razonable
const marker = layers[0];
map.setView(marker.getLatLng(), Math.min(config.zoom || 14, config.maxZoom || 18));
return;
}
// Varios marcadores: ajustar para mostrar todos
const bounds = markersLayer.getBounds();
if (bounds.isValid()) {
map.fitBounds(bounds, {
padding: [50, 50], // Margen de 50px
maxZoom: config.maxZoom || 18,
});
}
}
/**
* Crear icono de marcador circular SVG
*/
function createCircleMarkerIcon(color, isPilot = false) {
const size = 36;
const strokeWidth = 3;
// Crear SVG simple y limpio
let svg;
if (isPilot) {
// Marcador con estrella para pilotos
svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + size + '" height="' + (size + 12) + '" viewBox="0 0 ' + size + ' ' + (size + 12) + '">' +
'<circle cx="' + (size/2) + '" cy="' + (size/2) + '" r="' + (size/2 - 2) + '" fill="rgba(0,0,0,0.3)" transform="translate(2, 2)"/>' +
'<circle cx="' + (size/2) + '" cy="' + (size/2) + '" r="' + (size/2 - strokeWidth) + '" fill="' + color + '" stroke="white" stroke-width="' + strokeWidth + '"/>' +
'<polygon points="' + (size/2) + ',' + (size + 2) + ' ' + (size/2 + 5) + ',' + (size + 12) + ' ' + (size/2 - 5) + ',' + (size + 12) + '" fill="' + color + '" stroke="white" stroke-width="1"/>' +
'<text x="' + (size/2) + '" y="' + (size/2 + 4) + '" text-anchor="middle" fill="white" font-size="14" font-weight="bold">★</text>' +
'</svg>';
return L.divIcon({
className: 'fp-geo-circle-marker fp-geo-pilot-marker',
html: svg,
iconSize: [size, size + 12],
iconAnchor: [size/2, size + 12],
popupAnchor: [0, -(size + 12)],
});
} else {
// Marcador circular normal con punta
svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + size + '" height="' + (size + 12) + '" viewBox="0 0 ' + size + ' ' + (size + 12) + '">' +
'<circle cx="' + (size/2) + '" cy="' + (size/2) + '" r="' + (size/2 - 2) + '" fill="rgba(0,0,0,0.3)" transform="translate(2, 2)"/>' +
'<circle cx="' + (size/2) + '" cy="' + (size/2) + '" r="' + (size/2 - strokeWidth) + '" fill="' + color + '" stroke="white" stroke-width="' + strokeWidth + '"/>' +
'<polygon points="' + (size/2) + ',' + (size + 10) + ' ' + (size/2 + 6) + ',' + (size - 4) + ' ' + (size/2 - 6) + ',' + (size - 4) + '" fill="' + color + '" stroke="white" stroke-width="2" stroke-linejoin="round"/>' +
'</svg>';
return L.divIcon({
className: 'fp-geo-circle-marker',
html: svg,
iconSize: [size, size + 12],
iconAnchor: [size/2, size + 10],
popupAnchor: [0, -(size + 10)],
});
}
}
/**
* Añadir marcadores al mapa
*/
function addMarkers(mapData, markers) {
const { markersLayer, config } = mapData;
// Limpiar marcadores existentes
markersLayer.clearLayers();
mapData.allMarkers = [];
// Color por defecto
const defaultColor = config.markerDefaultColor || '#F97316';
markers.forEach(function(markerData) {
let icon;
let markerColor = defaultColor;
// es_piloto viene directamente del post (actuación), no de los términos
let isPilot = markerData.es_piloto === true;
// Buscar color de la taxonomía configurada para colores
if (config.useCategoryColors && markerData.taxonomies) {
// Primero intentar con la taxonomía de leyenda configurada
if (config.legendTaxonomy) {
const categoryTerms = markerData.taxonomies[config.legendTaxonomy];
if (categoryTerms && categoryTerms.length > 0) {
if (categoryTerms[0].color) {
markerColor = categoryTerms[0].color;
}
}
}
// Si no hay color, buscar en cualquier taxonomía
if (markerColor === defaultColor) {
for (const taxonomy in markerData.taxonomies) {
const terms = markerData.taxonomies[taxonomy];
if (terms && terms.length > 0 && terms[0].color) {
markerColor = terms[0].color;
break;
}
}
}
}
// Usar icono personalizado si existe, o círculo de color
if (config.markerIcon && config.markerIcon !== '') {
icon = L.icon({
iconUrl: config.markerIcon,
iconSize: [40, 50],
iconAnchor: [20, 50],
popupAnchor: [0, -50],
});
} else {
// Crear marcador circular con el color
icon = createCircleMarkerIcon(markerColor, isPilot);
}
// Crear marcador
const marker = L.marker([markerData.lat, markerData.lng], { icon: icon });
// Guardar datos en el marcador
marker.markerData = markerData;
marker.markerColor = markerColor;
// Evento de clic
marker.on('click', function() {
showDetail(mapData, markerData);
});
// Añadir al grupo
markersLayer.addLayer(marker);
mapData.allMarkers.push(marker);
});
// Actualizar contador
updateResultsCount(mapData, markers.length);
// Debug: mostrar en consola cuántos marcadores se añadieron
console.log('[FP Geo] Añadidos ' + markers.length + ' marcadores con color por defecto: ' + defaultColor);
}
/**
* Actualizar contador de resultados
*/
function updateResultsCount(mapData, count) {
const { config } = mapData;
const wrapper = document.getElementById(config.mapId + '-wrapper');
if (!wrapper) return;
const countEl = wrapper.querySelector('.fp-geo-results-number');
const labelEl = wrapper.querySelector('.fp-geo-results-label');
if (countEl) {
countEl.textContent = count;
}
if (labelEl) {
// Si solo hay un tipo de contenido, mostrar su nombre
if (config.postTypes && config.postTypes.length === 1) {
const postType = config.postTypes[0];
// Usar el nombre del post type en plural/singular
const labels = config.postTypeLabels || {};
if (count === 1) {
labelEl.textContent = labels[postType]?.singular || postType;
} else {
labelEl.textContent = labels[postType]?.plural || postType + 's';
}
} else {
labelEl.textContent = count === 1 ? 'resultado' : 'resultados';
}
}
}
/**
* Inicializar filtros
*/
function initFilters(mapData) {
const { config } = mapData;
const wrapper = document.getElementById(config.mapId + '-wrapper');
if (!wrapper) return;
const filtersEl = wrapper.querySelector('.fp-geo-filters');
if (!filtersEl) return;
// Botones de filtro
filtersEl.querySelectorAll('.fp-geo-filter-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
const taxonomy = btn.dataset.taxonomy;
const slug = btn.dataset.slug;
// Toggle activo
btn.classList.toggle('active');
// Actualizar filtros activos
if (!mapData.activeFilters[taxonomy]) {
mapData.activeFilters[taxonomy] = [];
}
const index = mapData.activeFilters[taxonomy].indexOf(slug);
if (index > -1) {
mapData.activeFilters[taxonomy].splice(index, 1);
} else {
mapData.activeFilters[taxonomy].push(slug);
}
// Aplicar filtros
applyFilters(mapData);
});
});
// Botón limpiar
const clearBtn = filtersEl.querySelector('.fp-geo-clear-filters');
if (clearBtn) {
clearBtn.addEventListener('click', function() {
// Limpiar todos los filtros
mapData.activeFilters = {};
filtersEl.querySelectorAll('.fp-geo-filter-btn.active').forEach(function(btn) {
btn.classList.remove('active');
});
applyFilters(mapData);
});
}
}
/**
* Aplicar filtros a los marcadores
*/
function applyFilters(mapData) {
const { markersLayer, allMarkers, activeFilters, config } = mapData;
const filterCombine = config.filterCombine || 'OR';
// Verificar si hay filtros activos
const hasActiveFilters = Object.values(activeFilters).some(arr => arr.length > 0);
let visibleCount = 0;
allMarkers.forEach(function(marker) {
const data = marker.markerData;
let shouldShow = true;
if (hasActiveFilters) {
if (filterCombine === 'AND') {
// Debe cumplir TODOS los filtros
shouldShow = Object.entries(activeFilters).every(function([taxonomy, slugs]) {
if (slugs.length === 0) return true;
const markerTerms = data.taxonomies && data.taxonomies[taxonomy];
if (!markerTerms) return false;
return slugs.some(function(slug) {
return markerTerms.some(function(term) {
return term.slug === slug;
});
});
});
} else {
// Debe cumplir AL MENOS UN filtro (OR)
shouldShow = Object.entries(activeFilters).some(function([taxonomy, slugs]) {
if (slugs.length === 0) return false;
const markerTerms = data.taxonomies && data.taxonomies[taxonomy];
if (!markerTerms) return false;
return slugs.some(function(slug) {
return markerTerms.some(function(term) {
return term.slug === slug;
});
});
});
// Si no hay filtros activos en ninguna taxonomía, mostrar todo
if (!Object.values(activeFilters).some(arr => arr.length > 0)) {
shouldShow = true;
}
}
}
if (shouldShow) {
markersLayer.addLayer(marker);
visibleCount++;
} else {
markersLayer.removeLayer(marker);
}
});
// Actualizar contador de resultados
updateResultsCount(mapData, visibleCount);
}
/**
* Inicializar panel de detalle
*/
function initDetailPanel(mapData) {
const { config } = mapData;
const wrapper = document.getElementById(config.mapId + '-wrapper');
if (!wrapper) return;
const detailEl = document.getElementById(config.mapId + '-detail');
const overlayEl = document.getElementById(config.mapId + '-overlay');
if (!detailEl) return;
// Botón cerrar
const closeBtn = detailEl.querySelector('.fp-geo-detail-close');
if (closeBtn) {
closeBtn.addEventListener('click', function() {
hideDetail(mapData);
});
}
// Cerrar al hacer clic en overlay (modal)
if (overlayEl) {
overlayEl.addEventListener('click', function() {
hideDetail(mapData);
});
}
// Cerrar con Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
hideDetail(mapData);
}
});
}
/**
* Mostrar detalle de un marcador
*/
function showDetail(mapData, data) {
const { config } = mapData;
const detailEl = document.getElementById(config.mapId + '-detail');
const overlayEl = document.getElementById(config.mapId + '-overlay');
if (!detailEl) return;
// Rellenar taxonomías
const taxEl = detailEl.querySelector('.fp-geo-detail-taxonomies');
if (taxEl) {
taxEl.innerHTML = '';
if (data.taxonomies) {
for (const taxonomy in data.taxonomies) {
data.taxonomies[taxonomy].forEach(function(term) {
const tag = document.createElement('span');
tag.className = 'fp-geo-detail-tax-tag';
tag.style.cssText = term.color ? 'background-color: ' + term.color + ';' : '';
let html = '';
if (term.icono) {
html += '<img src="' + term.icono + '" alt="" class="fp-geo-detail-tax-icon">';
}
html += '<span>' + term.name + '</span>';
tag.innerHTML = html;
taxEl.appendChild(tag);
});
}
}
}
// Thumbnail
const thumbEl = detailEl.querySelector('.fp-geo-detail-thumbnail');
if (thumbEl) {
if (data.thumbnail) {
thumbEl.innerHTML = '<img src="' + data.thumbnail + '" alt="' + data.title + '">';
thumbEl.classList.add('has-image');
} else {
thumbEl.innerHTML = '';
thumbEl.classList.remove('has-image');
}
}
// Título
const titleEl = detailEl.querySelector('.fp-geo-detail-title');
if (titleEl) {
titleEl.textContent = data.title;
}
// Excerpt
const excerptEl = detailEl.querySelector('.fp-geo-detail-excerpt');
if (excerptEl) {
excerptEl.textContent = data.excerpt || '';
}
// Ubicación
const locationEl = detailEl.querySelector('.fp-geo-detail-location');
if (locationEl) {
const locationParts = [data.direccion, data.localidad].filter(Boolean);
if (locationParts.length > 0) {
locationEl.innerHTML = '<span>' + locationParts.join(', ') + '</span>';
locationEl.style.display = 'flex';
} else {
locationEl.style.display = 'none';
}
}
// Contacto
const contactEl = detailEl.querySelector('.fp-geo-detail-contact');
if (contactEl) {
let contactHtml = '';
if (data.telefono) {
contactHtml += '<div class="fp-geo-detail-contact-item">📞 <a href="tel:' + data.telefono.replace(/\s/g, '') + '">' + data.telefono + '</a></div>';
}
if (data.email) {
contactHtml += '<div class="fp-geo-detail-contact-item">✉️ <a href="mailto:' + data.email + '">' + data.email + '</a></div>';
}
if (data.web) {
contactHtml += '<div class="fp-geo-detail-contact-item">🌐 <a href="' + data.web + '" target="_blank">Sitio web</a></div>';
}
contactEl.innerHTML = contactHtml;
}
// Link - actualizar texto si está configurado
const linkEl = detailEl.querySelector('.fp-geo-detail-link');
if (linkEl) {
linkEl.href = data.url;
if (config.detailButtonText) {
linkEl.textContent = config.detailButtonText;
}
}
// Footer - mostrar/ocultar según configuración
const footerEl = detailEl.querySelector('.fp-geo-detail-footer');
if (footerEl) {
footerEl.style.display = config.showDetailButton ? 'block' : 'none';
}
// Mostrar panel
detailEl.classList.add('visible');
if (overlayEl) {
overlayEl.classList.add('visible');
}
}
/**
* Ocultar panel de detalle
*/
function hideDetail(mapData) {
const { config } = mapData;
const detailEl = document.getElementById(config.mapId + '-detail');
const overlayEl = document.getElementById(config.mapId + '-overlay');
if (detailEl) {
detailEl.classList.remove('visible');
}
if (overlayEl) {
overlayEl.classList.remove('visible');
}
}
// Exponer API pública
window.FPGeoContent = {
getMap: function(mapId) {
return mapInstances[mapId];
},
refreshMarkers: function(mapId, markers) {
const mapData = mapInstances[mapId];
if (mapData) {
addMarkers(mapData, markers);
}
},
applyFilters: function(mapId) {
const mapData = mapInstances[mapId];
if (mapData) {
applyFilters(mapData);
}
},
fitToMarkers: function(mapId) {
const mapData = mapInstances[mapId];
if (mapData) {
fitMapToMarkers(mapData);
}
},
};
})();

View File

@@ -0,0 +1,313 @@
<?php
/**
* Plugin Name: FP Geo Content
* Plugin URI: https://freepress.coop
* Description: Mapa interactivo genérico con Leaflet y OpenStreetMap para mostrar contenido geolocalizado de WordPress.
* Version: 1.0.0
* Author: Freepress Coop
* Author URI: https://freepress.coop
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: fp-geo-content
* Domain Path: /languages
* Requires at least: 6.0
* Requires PHP: 8.0
*/
// Si se accede directamente, salir
if (!defined('ABSPATH')) {
exit;
}
// Constantes del plugin
define('FP_GEO_VERSION', '1.0.0');
define('FP_GEO_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('FP_GEO_PLUGIN_URL', plugin_dir_url(__FILE__));
define('FP_GEO_PLUGIN_BASENAME', plugin_basename(__FILE__));
/**
* Clase principal del plugin
*/
final class FP_Geo_Content {
/**
* Instancia única
*/
private static $instance = null;
/**
* Opciones del plugin
*/
private $options = [];
/**
* Obtener instancia única
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->load_dependencies();
$this->init_hooks();
$this->options = get_option('fp_geo_content_options', []);
}
/**
* Cargar dependencias
*/
private function load_dependencies() {
require_once FP_GEO_PLUGIN_DIR . 'includes/class-settings.php';
require_once FP_GEO_PLUGIN_DIR . 'includes/class-map-renderer.php';
require_once FP_GEO_PLUGIN_DIR . 'includes/class-data-provider.php';
}
/**
* Inicializar hooks
*/
private function init_hooks() {
// Activación
register_activation_hook(__FILE__, [$this, 'activate']);
register_deactivation_hook(__FILE__, [$this, 'deactivate']);
// Inicialización
add_action('init', [$this, 'init']);
add_action('plugins_loaded', [$this, 'load_textdomain']);
// Admin
if (is_admin()) {
add_action('admin_menu', [$this, 'add_admin_menu']);
add_action('admin_init', [$this, 'register_settings']);
add_action('admin_enqueue_scripts', [$this, 'admin_assets']);
}
// Frontend
add_action('wp_enqueue_scripts', [$this, 'register_frontend_assets']);
add_action('wp_head', [$this, 'output_custom_css'], 100);
// Shortcode
add_shortcode('fp-geo-map', [FP_Geo_Map_Renderer::class, 'render_shortcode']);
// AJAX
add_action('wp_ajax_fp_geo_get_markers', [FP_Geo_Data_Provider::class, 'ajax_get_markers']);
add_action('wp_ajax_nopriv_fp_geo_get_markers', [FP_Geo_Data_Provider::class, 'ajax_get_markers']);
}
/**
* Inicialización
*/
public function init() {
// Registrar settings
FP_Geo_Settings::register();
}
/**
* Cargar traducciones
*/
public function load_textdomain() {
load_plugin_textdomain(
'fp-geo-content',
false,
dirname(FP_GEO_PLUGIN_BASENAME) . '/languages/'
);
}
/**
* Añadir menú de administración
*/
public function add_admin_menu() {
add_options_page(
__('FP Geo Content', 'fp-geo-content'),
__('FP Geo Content', 'fp-geo-content'),
'manage_options',
'fp-geo-content',
[$this, 'render_settings_page']
);
}
/**
* Registrar ajustes
*/
public function register_settings() {
FP_Geo_Settings::register_settings();
}
/**
* Renderizar página de ajustes
*/
public function render_settings_page() {
include FP_GEO_PLUGIN_DIR . 'templates/settings-page.php';
}
/**
* Assets del admin
*/
public function admin_assets($hook) {
if ($hook !== 'settings_page_fp-geo-content') {
return;
}
// Cargar media uploader
wp_enqueue_media();
wp_enqueue_style(
'fp-geo-admin',
FP_GEO_PLUGIN_URL . 'assets/css/admin.css',
[],
FP_GEO_VERSION
);
wp_enqueue_script(
'fp-geo-admin',
FP_GEO_PLUGIN_URL . 'assets/js/admin.js',
['jquery', 'wp-media-utils'],
FP_GEO_VERSION,
true
);
}
/**
* Registrar assets del frontend (no cargar por defecto)
*/
public function register_frontend_assets() {
// Leaflet CSS
wp_register_style(
'leaflet',
'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css',
[],
'1.9.4'
);
// Leaflet MarkerCluster CSS
wp_register_style(
'leaflet-markercluster',
'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css',
['leaflet'],
'1.5.3'
);
wp_register_style(
'leaflet-markercluster-default',
'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css',
['leaflet-markercluster'],
'1.5.3'
);
// Plugin CSS
wp_register_style(
'fp-geo-content',
FP_GEO_PLUGIN_URL . 'assets/css/map.css',
['leaflet', 'leaflet-markercluster-default'],
FP_GEO_VERSION
);
// Leaflet JS
wp_register_script(
'leaflet',
'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js',
[],
'1.9.4',
true
);
// Leaflet MarkerCluster JS
wp_register_script(
'leaflet-markercluster',
'https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js',
['leaflet'],
'1.5.3',
true
);
// Plugin JS
wp_register_script(
'fp-geo-content',
FP_GEO_PLUGIN_URL . 'assets/js/map.js',
['leaflet', 'leaflet-markercluster'],
FP_GEO_VERSION,
true
);
}
/**
* Output custom CSS in head
*/
public function output_custom_css() {
$options = get_option('fp_geo_content_options', []);
$custom_css = $options['custom_css'] ?? '';
if (!empty(trim($custom_css))) {
echo "\n<style id=\"fp-geo-custom-css\">\n";
echo wp_strip_all_tags($custom_css);
echo "\n</style>\n";
}
}
/**
* Activación
*/
public function activate() {
// Opciones por defecto
$defaults = [
'post_types' => [],
'lat_field' => 'latitud',
'lng_field' => 'longitud',
'default_lat' => '40.4168',
'default_lng' => '-3.7038',
'default_zoom' => 12,
'min_zoom' => 5,
'max_zoom' => 18,
'cluster_enabled' => true,
'detail_display' => 'sidebar', // sidebar | modal
'filter_combine' => 'OR', // OR | AND
'tile_provider' => 'carto_light',
// Nuevas opciones
'scroll_wheel_zoom' => 'ctrl', // ctrl | always | disabled
'marker_icon' => 0,
'marker_default_color' => '#F97316',
'use_category_colors' => true,
'show_legend' => true,
'legend_taxonomy' => '',
'sidebar_position' => 'right', // left | right
'show_detail_button' => true,
'detail_button_text' => 'Ver detalle',
];
if (!get_option('fp_geo_content_options')) {
add_option('fp_geo_content_options', $defaults);
}
update_option('fp_geo_content_version', FP_GEO_VERSION);
}
/**
* Desactivación
*/
public function deactivate() {
// Nada que hacer por ahora
}
/**
* Obtener opción
*/
public static function get_option($key, $default = null) {
$options = get_option('fp_geo_content_options', []);
return $options[$key] ?? $default;
}
}
/**
* Iniciar el plugin
*/
function fp_geo_content() {
return FP_Geo_Content::get_instance();
}
// Arrancar
fp_geo_content();

View File

@@ -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;
}
}

View File

@@ -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();
}
}

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' => '',
],
];
}
}

View File

@@ -0,0 +1,179 @@
<?php
/**
* Template: Página de configuración
*
* @package FP_Geo_Content
*/
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="wrap fp-geo-settings">
<h1>
<span class="dashicons dashicons-location-alt"></span>
<?php _e('FP Geo Content - Configuración', 'fp-geo-content'); ?>
</h1>
<div class="fp-geo-settings-intro">
<p><?php _e('Configura el plugin para mostrar contenido geolocalizado en un mapa interactivo.', 'fp-geo-content'); ?></p>
</div>
<form method="post" action="options.php">
<?php
settings_fields('fp_geo_content_options');
do_settings_sections('fp-geo-content');
submit_button();
?>
</form>
<div class="fp-geo-settings-help">
<h2><?php _e('Uso del Shortcode', 'fp-geo-content'); ?></h2>
<p><?php _e('Usa el siguiente shortcode para mostrar el mapa en cualquier página o entrada:', 'fp-geo-content'); ?></p>
<code class="fp-geo-shortcode">[fp-geo-map]</code>
<h3><?php _e('Atributos disponibles', 'fp-geo-content'); ?></h3>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Atributo', 'fp-geo-content'); ?></th>
<th><?php _e('Descripción', 'fp-geo-content'); ?></th>
<th><?php _e('Ejemplo', 'fp-geo-content'); ?></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>post_types</code></td>
<td><?php _e('Tipos de contenido a mostrar (separados por coma)', 'fp-geo-content'); ?></td>
<td><code>post_types="actuacion,entidad"</code></td>
</tr>
<tr>
<td><code>taxonomies</code></td>
<td><?php _e('Taxonomías para filtros (separadas por coma)', 'fp-geo-content'); ?></td>
<td><code>taxonomies="iniciativa,linea_trabajo"</code></td>
</tr>
<tr>
<td><code>height</code></td>
<td><?php _e('Altura del mapa', 'fp-geo-content'); ?></td>
<td><code>height="500px"</code></td>
</tr>
<tr>
<td><code>lat</code> / <code>lng</code></td>
<td><?php _e('Centro inicial del mapa', 'fp-geo-content'); ?></td>
<td><code>lat="40.4168" lng="-3.7038"</code></td>
</tr>
<tr>
<td><code>zoom</code></td>
<td><?php _e('Nivel de zoom inicial (1-18)', 'fp-geo-content'); ?></td>
<td><code>zoom="14"</code></td>
</tr>
<tr>
<td><code>filters</code></td>
<td><?php _e('Mostrar barra de filtros', 'fp-geo-content'); ?></td>
<td><code>filters="true"</code></td>
</tr>
<tr>
<td><code>detail</code></td>
<td><?php _e('Modo de visualización del detalle', 'fp-geo-content'); ?></td>
<td><code>detail="sidebar"</code> o <code>detail="modal"</code></td>
</tr>
<tr>
<td><code>cluster</code></td>
<td><?php _e('Agrupar marcadores cercanos', 'fp-geo-content'); ?></td>
<td><code>cluster="true"</code></td>
</tr>
<tr>
<td><code>class</code></td>
<td><?php _e('Clases CSS adicionales', 'fp-geo-content'); ?></td>
<td><code>class="mi-mapa-custom"</code></td>
</tr>
<tr>
<td><code>sidebar_position</code></td>
<td><?php _e('Posición del panel lateral', 'fp-geo-content'); ?></td>
<td><code>sidebar_position="left"</code> o <code>sidebar_position="right"</code></td>
</tr>
<tr>
<td><code>legend</code></td>
<td><?php _e('Mostrar leyenda de colores', 'fp-geo-content'); ?></td>
<td><code>legend="true"</code></td>
</tr>
<tr>
<td><code>show_detail_btn</code></td>
<td><?php _e('Mostrar botón de detalle', 'fp-geo-content'); ?></td>
<td><code>show_detail_btn="true"</code></td>
</tr>
<tr>
<td><code>detail_btn_text</code></td>
<td><?php _e('Texto del botón de detalle', 'fp-geo-content'); ?></td>
<td><code>detail_btn_text="Ver más"</code></td>
</tr>
</tbody>
</table>
<h3><?php _e('Ejemplos', 'fp-geo-content'); ?></h3>
<pre><code>[fp-geo-map post_types="actuacion,entidad" height="600px" filters="true"]</code></pre>
<pre><code>[fp-geo-map post_types="evento_bloques" detail="modal" cluster="false"]</code></pre>
<pre><code>[fp-geo-map post_types="actuacion" sidebar_position="left" legend="true" detail_btn_text="Ver más información"]</code></pre>
<h3><?php _e('Personalización de estilos', 'fp-geo-content'); ?></h3>
<p><?php _e('Puedes personalizar los estilos del mapa usando las siguientes clases CSS:', 'fp-geo-content'); ?></p>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Selector', 'fp-geo-content'); ?></th>
<th><?php _e('Descripción', 'fp-geo-content'); ?></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>.fp-geo-wrapper</code></td>
<td><?php _e('Contenedor principal del mapa', 'fp-geo-content'); ?></td>
</tr>
<tr>
<td><code>.fp-geo-map</code></td>
<td><?php _e('Elemento del mapa Leaflet', 'fp-geo-content'); ?></td>
</tr>
<tr>
<td><code>.fp-geo-filters</code></td>
<td><?php _e('Barra de filtros', 'fp-geo-content'); ?></td>
</tr>
<tr>
<td><code>.fp-geo-filter-btn</code></td>
<td><?php _e('Botones de filtro', 'fp-geo-content'); ?></td>
</tr>
<tr>
<td><code>.fp-geo-detail</code></td>
<td><?php _e('Panel de detalle (sidebar o modal)', 'fp-geo-content'); ?></td>
</tr>
<tr>
<td><code>.fp-geo-sidebar</code></td>
<td><?php _e('Panel lateral', 'fp-geo-content'); ?></td>
</tr>
<tr>
<td><code>.fp-geo-modal</code></td>
<td><?php _e('Ventana modal', 'fp-geo-content'); ?></td>
</tr>
</tbody>
</table>
<h4><?php _e('Variables CSS personalizables', 'fp-geo-content'); ?></h4>
<pre><code>:root {
--fp-geo-primary: #1E6B52;
--fp-geo-secondary: #2A9D8F;
--fp-geo-accent: #F4A261;
--fp-geo-text: #333;
--fp-geo-bg: #fff;
--fp-geo-radius: 8px;
}</code></pre>
</div>
</div>