El geovisor
Un mapa del panel (Mapa de contenidos, ruta geo-admin-viewer, admin/geo/) al que cada módulo le registra
sus puntos. El geovisor no sabe de personas ni de organizaciones: pinta lo que le dan sus fuentes. El framework
trae dos, personas (Directory\Geo\PersonsGeoSource) y organizaciones (Organizations\Geo\OrganizationsGeoSource);
un clon añade las suyas sin tocar JavaScript.
Qué hace la pantalla
- Pide los puntos de todas las fuentes que el usuario puede ver, los agrupa en círculos con su cifra y los separa al acercarse; de cerca, cada punto con su imagen (o sus iniciales) y su nombre.
- «Viendo X de Y» avisa cuando hay más puntos de los que caben en una petición (la cota).
- Filtros y leyenda en paneles flotantes; apagar una fuente en la leyenda la quita del mapa.
- Al pulsar un punto pide su detalle y lo pinta en la tarjeta de su fuente.
- Calles o satélite. El centro y el acercamiento iniciales se configuran en Configuración › Integraciones › Llaves de MapBox, «Centro del mapa por omisión».
Sin llave de MapBox no hay mapa: la pantalla enseña el aviso mapbox-key-missing.
Tus datos en el mapa
Una clase que implementa PiecesPHP\Geo\GeoSource, una vista para su tarjeta y una línea en el *Routes de su módulo.
El ejemplo pone en el mapa los puntos de Ubicaciones (PiecesPHP\App\Locations\Mappers\PointMapper, tabla
locations_points), que existen en cualquier instalación. Cada clase va donde la cargue su espacio de nombres: con App\Geo, en la carpeta Geo de la carpeta App
de las clases de la aplicación, y la tarjeta a su lado (probado copiando el ejemplo tal cual el 2026-10-21):
<?php
namespace App\Geo;
use PiecesPHP\App\Locations\Mappers\PointMapper;
use PiecesPHP\Core\Database\ORM\Statements\WhereSegment;
use PiecesPHP\Geo\GeoPoint;
use PiecesPHP\Geo\GeoPointsResult;
use PiecesPHP\Geo\GeoQuery;
use PiecesPHP\Geo\GeoSource;
use PiecesPHP\Geo\GeoSourceSql;
class LocationPointsGeoSource implements GeoSource
{
public function key(): string { return 'location-points'; }
public function title(): string { return __(LOCATIONS_LANG_GROUP, 'Ubicaciones'); }
public function permissionRoute(): string { return 'locations-points-list'; }
public function marker(): string { return GeoSource::MARKER_DOT; }
public function colorVariable(): string { return '--geo-location-points-color'; }
public function filters(): array { return []; }
public function filterOptions(string $key, string $search, int $limit): array { return []; }
public function points(GeoQuery $query): GeoPointsResult
{
$table = PointMapper::PREFIX_TABLE . PointMapper::TABLE;
$model = PointMapper::model();
$model->select(["{$table}.id", "{$table}.name", "{$table}.longitude", "{$table}.latitude"]);
$model->where(GeoSourceSql::addBboxWhere(new WhereSegment(), "{$table}.longitude", "{$table}.latitude", $query));
[$total, $rows] = GeoSourceSql::countAndFetch($model, 'id ASC', $query->limit());
$points = [];
foreach ($rows as $row) {
$points[] = new GeoPoint((int) $row['id'], (float) $row['longitude'], (float) $row['latitude'], (string) $row['name']);
}
return new GeoPointsResult($points, $total);
}
public function pointDetail(int $id): ?array
{
$point = new PointMapper($id);
return $point->id !== null ? ['name' => $point->name, 'address' => $point->address] : null;
}
public function thumbnailSource(int $id): ?string { return null; }
public function cardView(): string { return __DIR__ . '/card.php'; }
}
La tarjeta (card.php) es marcado fijo: el geovisor escribe cada clave de pointDetail() como texto en el
elemento con su data-geo-field, y pone en href la de data-geo-href. Nada de PHP con datos dentro:
<div class="geo-card">
<div class="geo-card-title" data-geo-field="name"></div>
<div class="geo-card-line" data-geo-field="address"></div>
</div>
Y el registro, en el *Routes de tu módulo, junto a sus rutas (así lo hacen DirectoryRoutes y
OrganizationsRoutes):
El color se declara como variable CSS en una hoja del módulo: --geo-location-points-color: var(--main-brand-color);.
El contrato, método a método
| Método | Qué devuelve | Lo que no se puede olvidar |
|---|---|---|
key() |
La clave única de la fuente | Solo letras, números, - y _: viaja en las URL |
title() |
El nombre en la leyenda, ya traducido | Dentro de __() |
permissionRoute() |
El nombre de una ruta existente | Quien no tiene permiso sobre ESA ruta no ve la fuente ni pide sus puntos, su detalle ni sus miniaturas |
marker() |
GeoSource::MARKER_DOT o GeoSource::MARKER_AVATAR |
Con MARKER_AVATAR, cada punto lleva su imagen o, sin ella, las iniciales |
colorVariable() |
El nombre de una variable CSS (--geo-…) |
Nunca un color escrito |
filters() |
GeoFilter[] (texto, opciones, u opciones remotas con GeoFilter::TYPE_REMOTE_OPTIONS) |
Los que no declara, los ignora; un valor que no sirve lanza GeoQueryException. No consulta la base: un filtro con muchas opciones se declara remoto |
filterOptions() |
Las opciones de un filtro remoto que casan con la búsqueda, valor → texto sin escapar, hasta $limit |
Un filtro que no es suyo o no es remoto: []. Las pide el desplegable al escribir, por geo-admin-filter-options, con el permiso del geovisor y hasta 20 |
points() |
GeoPointsResult con los puntos del encuadre y el total |
El encuadre en WHERE con GeoSourceSql::addBboxWhere(), y total y página en UNA consulta con GeoSourceSql::countAndFetch(); respeta $query->limit() |
pointDetail() |
Las claves de la tarjeta, o null |
null si el punto no saldría en points() (oculto, inactivo, sin permiso): si no, la tarjeta filtra lo que el mapa esconde |
thumbnailSource() |
El archivo ORIGINAL de la imagen en disco, o null |
Nunca una URL. Misma visibilidad que pointDetail() |
cardView() |
La ruta absoluta de la vista de su tarjeta | Solo marcado fijo con data-geo-field y data-geo-href |
La imagen de un punto (solo con MARKER_AVATAR) va como último argumento de GeoPoint: la URL que da
PiecesPHP\Geo\GeoThumbnail::url($clave, $id, $archivoOriginal). La miniatura de 64 px (128 en una pantalla de alta densidad,
que el visor pide sola) se genera una vez en app/cache/geo-thumbnails/, se regenera si el original cambia —otro
archivo, otra fecha u otro tamaño— y se sirve por geo-admin-thumbnail con el permiso de la
fuente, así que un archivo privado no se publica. bin/cli clean-cache borra las miniaturas de los puntos que su fuente
ya no daría (GeoThumbnail::sweep()).
Para cambiar lo que viene
- Otra tarjeta para personas u organizaciones: hereda su fuente, cambia
cardView()y registra la tuya conGeoSourceRegistry::replace(). - Quitar una fuente:
GeoSourceRegistry::remove('persons')después de que su módulo la registre. - Colores y tamaños: las variables CSS del geovisor.
Para medirlo
bin/cli geo-load-fixture escribir=yes cantidad=<n> (de 1 a 20.000, solo en local) escribe personas y organizaciones
zz-carga- repartidas por Barranquilla; limpiar=yes las borra. Con 10.000 puntos (medido el 2026-10-21): mover y
acercar no bloquean; la entrada tarda alrededor de un segundo, porque la vista general pide todos los puntos para
agruparlos.
Trampas
- El marcador de una condición se fabrica con su texto: una expresión con
$,^,?o+dentro deaddBboxWhere()rompe la consulta («mixed named and positional parameters»). Escribe$comoCHAR(36), como hace la fuente de organizaciones. - Las columnas de coordenadas, con índice: sin él, el encuadre lee la tabla entera. Personas usa el índice
geo(longitude,latitude) deuser_system_profile.