Commit c75dbfb2d4c3404a6fd0f34d17413e16ae71bd43

Authored by Antigravity AI
1 parent f16256b4

Fix MapLibre proxy routing and enforce Regla 30

FdwService.java
... ... @@ -25,7 +25,8 @@ public class FdwService {
25 25 String sqlEntidad = "SELECT sigem_site, sigem_dbname, boundno, boundse, latlong, zoom, maxzoom, minzoom FROM public.entidades WHERE entidad = ?";
26 26 List<Map<String, Object>> entidades = masterJdbcTemplate.queryForList(sqlEntidad, Integer.parseInt(entidadId));
27 27  
28   - if (entidades.isEmpty()) throw new RuntimeException("Entidad " + entidadId + " no encontrada.");
  28 + if (entidades.isEmpty())
  29 + throw new RuntimeException("Entidad " + entidadId + " no encontrada.");
29 30  
30 31 Map<String, Object> data = entidades.get(0);
31 32 String sigemSite = (String) data.get("sigem_site");
... ... @@ -44,17 +45,25 @@ public class FdwService {
44 45 try {
45 46 gisJdbcTemplate.execute("CREATE EXTENSION IF NOT EXISTS postgres_fdw");
46 47 gisJdbcTemplate.execute("DROP SERVER IF EXISTS " + serverName + " CASCADE");
47   - gisJdbcTemplate.execute(String.format("CREATE SERVER %s FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '%s', port '%s', dbname '%s')", serverName, host, port, sigemDbname));
48   - gisJdbcTemplate.execute(String.format("CREATE USER MAPPING FOR sigem_user SERVER %s OPTIONS (user '%s', password '%s')", serverName, user, pass));
  48 + gisJdbcTemplate.execute(String.format(
  49 + "CREATE SERVER %s FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '%s', port '%s', dbname '%s')",
  50 + serverName, host, port, sigemDbname));
  51 + gisJdbcTemplate.execute(
  52 + String.format("CREATE USER MAPPING FOR sigem_user SERVER %s OPTIONS (user '%s', password '%s')",
  53 + serverName, user, pass));
49 54 gisJdbcTemplate.execute("CREATE SCHEMA IF NOT EXISTS " + schemaName);
50   - gisJdbcTemplate.execute(String.format("IMPORT FOREIGN SCHEMA public LIMIT TO (v_liq_entidad_totalxobjeto, v_liq_entidad_percentiles, usuarios, ventanas_usuario) FROM SERVER %s INTO %s", serverName, schemaName));
  55 + gisJdbcTemplate.execute(String.format(
  56 + "IMPORT FOREIGN SCHEMA public LIMIT TO (v_liq_entidad_totalxobjeto, v_liq_entidad_percentiles, usuarios, ventanas_usuario) FROM SERVER %s INTO %s",
  57 + serverName, schemaName));
51 58  
52 59 // REGLA 23 ACTUALIZADA: Join OBLIGATORIO por CCC = INM_CTACATASTRAL
53 60 String tableLotes = "public.e" + entidadId + "_lotes_conccc";
54 61 String viewName = "vw_lotes_morosidad_" + entidadId;
55 62 String viewWms = "vw_lotes_wms_" + entidadId;
56   - String sqlJoin = String.format("CREATE OR REPLACE VIEW public.%s AS SELECT l.*, m.inm_ficha, m.inm_ctacatastral, m.trb_total_deuda, m.trb_total_pago, m.ultimo_pago FROM %s l LEFT JOIN %s.v_liq_entidad_totalxobjeto m ON l.ccc = m.inm_ctacatastral", viewName, tableLotes, schemaName);
57   -
  63 + String sqlJoin = String.format(
  64 + "CREATE OR REPLACE VIEW public.%s AS SELECT l.*, m.inm_ficha, m.inm_ctacatastral, m.trb_total_deuda, m.trb_total_pago, m.ultimo_pago FROM %s l LEFT JOIN %s.v_liq_entidad_totalxobjeto m ON l.ccc = m.inm_ctacatastral",
  65 + viewName, tableLotes, schemaName);
  66 +
58 67 gisJdbcTemplate.execute(sqlJoin);
59 68 gisJdbcTemplate.execute(sqlJoin.replace(viewName, viewWms));
60 69  
... ... @@ -64,15 +73,20 @@ public class FdwService {
64 73 geoServerService.truncateCache(viewName);
65 74 geoServerService.publishLayer(viewWms, viewWms, "morosidad_style_wms", boundNo, boundSe);
66 75 geoServerService.truncateCache(viewWms);
67   - } catch (Exception e) { System.err.println("Advertencia GS: " + e.getMessage()); }
  76 + } catch (Exception e) {
  77 + System.err.println("Advertencia GS: " + e.getMessage());
  78 + }
68 79  
69   - } catch (Exception e) { throw new RuntimeException("Error FDW: " + e.getMessage(), e); }
  80 + } catch (Exception e) {
  81 + throw new RuntimeException("Error FDW: " + e.getMessage(), e);
  82 + }
70 83 }
71 84  
72 85 private String extractParam(String siteParam, String key, String defaultValue) {
73 86 if (siteParam != null && siteParam.contains(key + "=")) {
74 87 String[] parts = siteParam.split(key + "=");
75   - if (parts.length > 1) return parts[1].split(" ")[0].trim();
  88 + if (parts.length > 1)
  89 + return parts[1].split(" ")[0].trim();
76 90 }
77 91 return defaultValue;
78 92 }
... ... @@ -91,8 +105,12 @@ public class FdwService {
91 105 conn.setRequestProperty("Content-Type", "text/xml");
92 106 conn.setDoOutput(true);
93 107 conn.setConnectTimeout(2000);
94   - try (java.io.OutputStream os = conn.getOutputStream()) { os.write(xmlBody.getBytes()); }
  108 + try (java.io.OutputStream os = conn.getOutputStream()) {
  109 + os.write(xmlBody.getBytes());
  110 + }
95 111 conn.getResponseCode();
96   - } catch (Exception e) { System.err.println("Error MVT: " + e.getMessage()); }
  112 + } catch (Exception e) {
  113 + System.err.println("Error MVT: " + e.getMessage());
  114 + }
97 115 }
98 116 }
... ...
GIS-GEOSERVER-REGLAS.md
... ... @@ -4,12 +4,20 @@ Regla 0. Eres un senior fullstack developer con experiencia reconocida en Base d
4 4 El ecosistema principal es Java 21 con Spring Boot y deberá usarse de forma preferencial todas las veces que se pueda.
5 5 El frontend usa el framework corporativo AdminLTE/Bootstrap.
6 6  
  7 +Este proyecto GIS-GEOSERVER debe utilizar un patrón arquitectónico llamado Monolito Spring Boot con Recursos Embebidos (Server-Side Monolith).
  8 +
  9 +El Backend (El Cerebro): Está escrito en Java 21 con Spring Boot (ubicado en la carpeta src/main/java/). Se encarga de la lógica pesada: seguridad, conexión a la base de datos PostgreSQL/PostGIS, consultas FDW, y expone los servicios REST (ej. /api/gis/entidad/505).
  10 +
  11 +El Frontend (La Cara): Utiliza tecnologías de navegador clásicas: HTML, Vanilla JavaScript, jQuery y AdminLTE/Bootstrap. Pero en lugar de vivir en un servidor web aparte (como un Nginx puro o un Node.js), todos estos archivos viven dentro de la carpeta del propio proyecto Java: en src/main/resources/static/ y src/main/resources/templates/.
  12 +
  13 +No hay dos servidores distintos. Cuando Spring Boot arranca su servidor interno (Tomcat), hace el trabajo doble: Si el navegador le pide datos, el código Java responde con JSON (API REST). Si el navegador le pide /gis-geoserver/mapas, el mismo servidor Java va a su propia carpeta interna (static o templates), lee el archivo HTML, y se lo envía al navegador del usuario. Cuando se ejecute el comando de Maven (mvnw clean package) se empaquetará tanto el código Java como los archivos HTML dentro de un único archivo .jar. Todo se levanta en un solo contenedor Docker y el proxy de Apache enruta todo bajo el mismo prefijo (/gis-geoserver/).
  14 +
7 15 Regla 1. El ambiente de desarrollo y compilación se encuentra en el 192.168.1.123.
8 16 El JENKINS a usar está en este servidor. Los comandos del jenkins se ejecutan en el servidor 192.168.1.123.
9 17 El MAVEN a usar está en este servidor. Los comandos maven se ejecutan en el servidor 192.168.1.123.
10 18 El DOCKER a usar está en el servidor 192.168.1.123. Los comandos docker se ejecutan en el servidor192.168.1.123.
11 19 Todas las compilaciones se ejecutarán en el servidor 192.168.1.123.
12   -La base de datos georreferenciada SIGEM del Postgis está en este servidor y el superuser es el usuario registrado en el motor de base de datos como:
  20 +La base de datos georreferenciada física es SIGEM del Postgis está en este servidor y el superuser es el usuario registrado en el motor de base de datos como:
13 21 Usuario: sigem_user
14 22 Contraseña: sigem_pass
15 23  
... ... @@ -18,6 +26,7 @@ Regla 2. Las bases de datos alfanuméricas de los municipios a usar vinculadas a
18 26 Regla 3. El proxypass principal de redireccionamiento reside en el servidor 192.168.1.10
19 27 El proxypass maestro de redireccionamiento reside en el servidor 192.168.1.20
20 28 Estos proxypass no deben ser modificados por ningún motivo.
  29 +El proxy de Nginx de la Regla 3 tiene GeoServer mapeado a la ruta /geoserver/. Las peticiones se deberán mantener en el mismo dominio para evitar bloqueos CORS y para que sean enrutadas correctamente al contenedor de GeoServer.
21 30  
22 31 Regla 4. Para el LOGIN, para el campo desplegable de las ENTIDADES (Municipios) los datos deben ser obtenidos de la tabla ENTIDADES del SIGEMWEB del servidor 192.168.1.254.
23 32 Para el LOGIN, el usuario debe utilizar su usuario y contraseña del SIGEM del municipio.
... ... @@ -54,11 +63,18 @@ Regla 11. Jenkins (.123): admin / x25yvaga2024.
54 63  
55 64 Regla 12. Jenkins SSH Credential ID: sigem-server-123 (root).
56 65  
57   -Regla 13. Tomcat Manager: manager / x25yvaga2023. GeoServer Web UI: admin / geoserver.
  66 +Regla 13.
  67 +Tomcat Manager: manager / x25yvaga2023.
  68 +GeoServer Web UI: admin / geoserver.
  69 +
  70 +Regla 14.
  71 +Endpoints Geoserver (.123:8080): /geoserver/wms, /geoserver/wfs, /geoserver/rest.
58 72  
59   -Regla 14. Endpoints Geoserver (.123:8080): /geoserver/wms, /geoserver/wfs, /geoserver/rest.
  73 +User: admin
  74 +Password: x25yvaga2023
60 75  
61   -Regla 15. La aplicación se desplegará en el servidor 192.168.1.123
  76 +Regla 15.
  77 +La aplicación se desplegará en el servidor 192.168.1.123
62 78 La carpeta de trabajo es: /yvyape/proyectos/sigem-gis
63 79  
64 80 Regla 16. Conexión FDW y Sincronización de Vistas.
... ... @@ -66,7 +82,10 @@ Debe verificarse la existencia del FDW del municipio en cada LOGIN.
66 82 Si no existe, crearlo.
67 83 Refrescar obligatoriamente las vistas `vw_lotes_morosidad_X`.
68 84  
69   -Regla 17. Git (.100): cbareiro@yvaga.com.py / carlos57. Repo: git@git.yvaga.com.py:geo/gis-geoserver.git.
  85 +Regla 17. GIT
  86 +El repositorio de Git se encuentra en git.yvaga.com.py en el servidor 192.168.1.100
  87 +Credenciales de Git (.100): cbareiro@yvaga.com.py / carlos57. Repo: git@git.yvaga.com.py:geo/gis-geoserver.git.
  88 +Si la carpeta a usar ya tiene archivos, el comando puede fallar, y debe limpiarse antes de clonar.
70 89  
71 90 Regla 18. SSH Local: Usar Bitvise con usuario cbareiro.
72 91  
... ... @@ -78,10 +97,17 @@ Regla 20. Prefijo FrontEnd: /gis-geoserver/.
78 97 Regla 21. ContextPath Backend: /gis-geoserver.
79 98  
80 99 Regla 22: Integridad de Comandos Remotos:
81   -Se utilizarán comandos disponibles en Bitvise.
82   -Los accesos a otros servidores se realizarán mediante SSH y sftp.
  100 +Desde el ambiente de desarrollo (Windows 11) se utilizará SSH de Bitvise para los accesos a otros servidores, usando la Regla 9.
  101 +Debe usarse -cmdQuoted para ejecutar comandos complejos.
  102 +
83 103 Queda prohibido el uso de comandos printf, echo o concatenaciones multilínea complejas para crear archivos. Usar sftpc para subir archivos íntegros.
84   -Para los comandos SQL complejos debido a la interpretación del shell de Windowsy para asegurar la integridad total (Regla 22), se deben subir los archivos de estructura y población por SFTP ejecutándolos desde el respectivo servidor. El paso intermedio para no generar errores es preparar los archivos de comandos en el servidor local y luego copiar el archivo de comandos al interior del contenedor antes de su ejecución.
  104 +Para los comandos SQL complejos debido a la interpretación del shell de Windows y para asegurar la integridad total (Regla 22), se deben subir los archivos de estructura y población por SFTPC ejecutándolos desde el respectivo servidor. El paso intermedio para no generar errores es preparar los archivos de comandos en el servidor local y luego copiar el archivo de comandos al interior del contenedor antes de su ejecución, usando sftpc para subir archivos íntegros.
  105 +
  106 +Por ejemplo:
  107 +…\GIS-GEOSERVER > sexec cbareiro@192.168.1.123 -pw=x25yvaga2023 -cmd="cd /yvyape/proyectos/sigem-gis"
  108 +…\GIS-GEOSERVER > sexec cbareiro@192.168.1.123 -pw=x25yvaga2023 -cmd="./mvnw clean package -DskipTests"
  109 +…\GIS-GEOSERVER > sftpc cbareiro@192.168.1.123 -pw=x25yvaga2023 -cmd="put -o docker-compose.yml.remote.tmp /yvyape/proyectos/sigem-gis/docker-compose.yml"
  110 +…\GIS-GEOSERVER > sexec cbareiro@192.168.1.123 -pw=x25yvaga2023 -cmdQuoted -cmd="curl -s -u admin:geoserver -X PUT -H 'Content-Type: application/xml' -d @/yvyape/proyectos/sigem-gis/geoserver_config.xml http://proyecto-geoserver-1:8080/geoserver/rest/workspaces/sigem/datastores/sigem/featuretypes/vw_lotes_morosidad_505.xml"
85 111  
86 112 Regla 23. Columnas de Unión (Joins). Standard SNC.
87 113 Para toda vista de unión con el FDW, se debe utilizar la columna `snc_cuenta` (limpia) contra `REPLACE(liq.inm_ctacatastral, '-', '')`.
... ... @@ -144,13 +170,13 @@ La inserción en la base de datos se realizará mediante el uso directo de ST_Ge
144 170 Implementar un TRUNCATE automático al inicio del proceso de importación. Si al importar los números son UTM, se deben transformar a 4326 antes de guardarlos.
145 171 Las columnas de las tablas eXXX_lotes_activos deberá tener todas las columnas del SNC.
146 172  
147   -
148 173 Regla 29.
149   -Para la construcción en la compilación, se usa JAVA21 del 192.168.1.123, en sincronía con la Regla 1.
  174 +Para la construcción en la compilación, se usa JAVA21 del 192.168.1.123, en sincronía con la Regla 1.
  175 +El uso de rutas relativas en Docker ya ha estado causando problemas desde antes. Debes usar rutas absolutas
150 176  
151 177 Regla 30. Arquitectura de Visualización en Dos Niveles.
152 178 El visor GIS utiliza una estrategia de superposición para optimizar el rendimiento y la claridad visual. Utiliza dos tipos de capas para mostrar los lotes: Una Capa Base (Lotes Grises) y múltiples Capas Temáticas (Lotes Coloreados).
153 179 1. Capa Base (Esqueleto): Utiliza la tabla física soberana `eXXX_lotes_activos`. Se publica en GeoServer con un estilo neutro (gris/transparente). Representa la totalidad del territorio y siempre es local y persistente.
154 180 2. Capa Temática (Inteligencia): Utiliza la vista lógica `vw_lotes_morosidad_XXX`. Es la capa dinámica que aplica el JOIN con el FDW municipal.
155 181 El Efecto Visual: La capa temática se superpone a la base. Aquellos lotes con coincidencia tributaria se "pintan" con el estilo de morosidad (semáforo), mientras que los lotes sin vínculo o sin deuda permanecen en gris (dejando ver la capa base), garantizando que la ciudad nunca se vea "incompleta".
156   -
  182 +El contenedor de GeoServer (sigem-gis-geoserver-1) y el de PostgreSQL (proyecto-postgres-1) tienen sus propias redes internas de Docker y no tienen acceso directo a otras redes Docker por seguridad y estabilidad. Por lo tanto, debe configurarse y ejecutarse dentro de la red privada interna de Docker.
157 183 \ No newline at end of file
... ...
apply_config.sh 0 → 100644
  1 +#!/bin/bash
  2 +curl -s -u admin:geoserver -X PUT -H "Content-Type: application/xml" -d @/yvyape/proyectos/sigem-gis/geoserver_config.xml http://proyecto-geoserver-1:8080/geoserver/rest/workspaces/sigem/datastores/sigem/featuretypes/vw_lotes_morosidad_505.xml
... ...
apply_config_v2.sh 0 → 100644
  1 +#!/bin/bash
  2 +curl -v -u admin:x25yvaga2023 -X PUT -H "Content-Type: application/xml" -d @/yvyape/proyectos/sigem-gis/geoserver_config.xml http://localhost:8080/geoserver/rest/workspaces/sigem/datastores/sigem_datastore/featuretypes/vw_lotes_morosidad_505.xml
... ...
apply_config_v3.sh 0 → 100644
  1 +#!/bin/bash
  2 +curl -v -u admin:geoserver -X PUT -H "Content-Type: application/xml" -d @/yvyape/proyectos/sigem-gis/geoserver_config.xml http://localhost:8080/geoserver/rest/workspaces/sigem/datastores/sigem_datastore/featuretypes/vw_lotes_morosidad_505.xml
... ...
apply_config_v4.sh 0 → 100644
  1 +#!/bin/bash
  2 +curl -v -u admin:geoserver -X PUT -H "Content-Type: text/xml" -d @/yvyape/proyectos/sigem-gis/geoserver_config.xml http://localhost:8080/geoserver/rest/workspaces/sigem/datastores/sigem_db/featuretypes/vw_lotes_morosidad_505.xml
... ...
geoserver_config.xml 0 → 100644
  1 +<featureType>
  2 + <name>vw_lotes_morosidad_505</name>
  3 + <cqlFilter>trb_tributo = 'INM'</cqlFilter>
  4 +</featureType>
... ...
mapas.html
1 1 <!DOCTYPE html>
2 2 <html lang="es">
  3 +
3 4 <head>
4 5 <meta charset="UTF-8">
5 6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
... ... @@ -9,57 +10,209 @@
9 10 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
10 11 <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css">
11 12 <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
12   -
  13 +
13 14 <style>
14   - body { font-family: 'Roboto', sans-serif; height: 100vh; background: #0b0e14; overflow: hidden; }
15   - #map { position: absolute; top: 0; bottom: 0; left: 0; right: 0; }
16   -
  15 + body {
  16 + font-family: 'Roboto', sans-serif;
  17 + height: 100vh;
  18 + background: #0b0e14;
  19 + overflow: hidden;
  20 + }
  21 +
  22 + #map {
  23 + position: absolute;
  24 + top: 0;
  25 + bottom: 0;
  26 + left: 0;
  27 + right: 0;
  28 + }
  29 +
17 30 /* Tema Oscuro Personalizado */
18   - .main-sidebar { background-color: #0f172a !important; width: 280px !important; }
19   - .content-wrapper { background: transparent !important; }
20   - .main-header { background-color: #0f172a !important; border-bottom: 1px solid #1e293b !important; }
21   -
22   - .nav-link p { color: #94a3b8; font-weight: 500; }
23   - .nav-link.active { background-color: #3b82f6 !important; color: white !important; }
24   - .nav-link.active p { color: white !important; }
25   -
26   - .info-box { background: #1e293b; color: white; border-radius: 8px; margin-bottom: 15px; border: 1px solid #334155; }
27   - .info-box-text { color: #94a3b8; text-transform: uppercase; font-size: 0.7rem; font-weight: 700; }
28   - .info-box-number { font-size: 1.2rem; color: #fff; }
29   -
30   - .map-section-title { color: #475569; font-size: 0.65rem; font-weight: 700; text-transform: uppercase; padding: 10px 15px; letter-spacing: 1px; }
31   -
  31 + .main-sidebar {
  32 + background-color: #0f172a !important;
  33 + width: 280px !important;
  34 + }
  35 +
  36 + .content-wrapper {
  37 + background: transparent !important;
  38 + }
  39 +
  40 + .main-header {
  41 + background-color: #0f172a !important;
  42 + border-bottom: 1px solid #1e293b !important;
  43 + }
  44 +
  45 + .nav-link p {
  46 + color: #94a3b8;
  47 + font-weight: 500;
  48 + }
  49 +
  50 + .nav-link.active {
  51 + background-color: #3b82f6 !important;
  52 + color: white !important;
  53 + }
  54 +
  55 + .nav-link.active p {
  56 + color: white !important;
  57 + }
  58 +
  59 + .info-box {
  60 + background: #1e293b;
  61 + color: white;
  62 + border-radius: 8px;
  63 + margin-bottom: 15px;
  64 + border: 1px solid #334155;
  65 + }
  66 +
  67 + .info-box-text {
  68 + color: #94a3b8;
  69 + text-transform: uppercase;
  70 + font-size: 0.7rem;
  71 + font-weight: 700;
  72 + }
  73 +
  74 + .info-box-number {
  75 + font-size: 1.2rem;
  76 + color: #fff;
  77 + }
  78 +
  79 + .map-section-title {
  80 + color: #475569;
  81 + font-size: 0.65rem;
  82 + font-weight: 700;
  83 + text-transform: uppercase;
  84 + padding: 10px 15px;
  85 + letter-spacing: 1px;
  86 + }
  87 +
32 88 /* Widget Superior */
33   - .top-widget { position: absolute; top: 15px; left: 50%; transform: translateX(-50%); background: rgba(15, 23, 42, 0.9); padding: 8px 25px; border-radius: 50px; border: 1px solid #334155; color: white; font-weight: 700; font-size: 0.85rem; z-index: 10; box-shadow: 0 4px 20px rgba(0,0,0,0.5); backdrop-filter: blur(5px); }
  89 + .top-widget {
  90 + position: absolute;
  91 + top: 15px;
  92 + left: 50%;
  93 + transform: translateX(-50%);
  94 + background: rgba(15, 23, 42, 0.9);
  95 + padding: 8px 25px;
  96 + border-radius: 50px;
  97 + border: 1px solid #334155;
  98 + color: white;
  99 + font-weight: 700;
  100 + font-size: 0.85rem;
  101 + z-index: 10;
  102 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
  103 + backdrop-filter: blur(5px);
  104 + }
34 105  
35 106 /* Leyenda Inferior Derecha */
36   - .legend-card { position: absolute; bottom: 30px; right: 20px; width: 220px; background: rgba(15, 23, 42, 0.9); border-radius: 12px; border: 1px solid #334155; padding: 15px; color: white; z-index: 10; box-shadow: 0 4px 20px rgba(0,0,0,0.5); display: none; }
37   - .legend-title { font-size: 0.7rem; font-weight: 800; color: #3b82f6; text-transform: uppercase; margin-bottom: 10px; border-bottom: 1px solid #334155; padding-bottom: 5px; }
38   - .legend-item { display: flex; align-items: center; margin-bottom: 4px; font-size: 0.75rem; color: #cbd5e1; }
39   - .legend-color { width: 15px; height: 10px; border-radius: 2px; margin-right: 12px; }
  107 + .legend-card {
  108 + position: absolute;
  109 + bottom: 30px;
  110 + right: 20px;
  111 + width: 220px;
  112 + background: rgba(15, 23, 42, 0.9);
  113 + border-radius: 12px;
  114 + border: 1px solid #334155;
  115 + padding: 15px;
  116 + color: white;
  117 + z-index: 10;
  118 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
  119 + display: none;
  120 + }
  121 +
  122 + .legend-title {
  123 + font-size: 0.7rem;
  124 + font-weight: 800;
  125 + color: #3b82f6;
  126 + text-transform: uppercase;
  127 + margin-bottom: 10px;
  128 + border-bottom: 1px solid #334155;
  129 + padding-bottom: 5px;
  130 + }
  131 +
  132 + .legend-item {
  133 + display: flex;
  134 + align-items: center;
  135 + margin-bottom: 4px;
  136 + font-size: 0.75rem;
  137 + color: #cbd5e1;
  138 + }
  139 +
  140 + .legend-color {
  141 + width: 15px;
  142 + height: 10px;
  143 + border-radius: 2px;
  144 + margin-right: 12px;
  145 + }
40 146  
41 147 /* Estilo Botones Menú MVT/PNG */
42   - .btn-map-mode { background: #1e293b; border: 1px solid #334155; border-radius: 6px; margin: 5px 15px; padding: 10px; cursor: pointer; transition: 0.3s; display: flex; align-items: center; color: #cbd5e1; font-size: 0.85rem; }
43   - .btn-map-mode:hover { background: #334155; }
44   - .btn-map-mode.active { background: #3b82f6; color: white; border-color: #60a5fa; box-shadow: 0 0 15px rgba(59, 130, 246, 0.4); }
45   - .btn-map-mode i { margin-right: 12px; font-size: 1rem; }
  148 + .btn-map-mode {
  149 + background: #1e293b;
  150 + border: 1px solid #334155;
  151 + border-radius: 6px;
  152 + margin: 5px 15px;
  153 + padding: 10px;
  154 + cursor: pointer;
  155 + transition: 0.3s;
  156 + display: flex;
  157 + align-items: center;
  158 + color: #cbd5e1;
  159 + font-size: 0.85rem;
  160 + }
  161 +
  162 + .btn-map-mode:hover {
  163 + background: #334155;
  164 + }
  165 +
  166 + .btn-map-mode.active {
  167 + background: #3b82f6;
  168 + color: white;
  169 + border-color: #60a5fa;
  170 + box-shadow: 0 0 15px rgba(59, 130, 246, 0.4);
  171 + }
  172 +
  173 + .btn-map-mode i {
  174 + margin-right: 12px;
  175 + font-size: 1rem;
  176 + }
  177 +
  178 + .btn-sync {
  179 + background: rgba(34, 197, 94, 0.1);
  180 + border: 1px dashed #22c55e;
  181 + color: #22c55e;
  182 + padding: 8px;
  183 + margin: 15px;
  184 + border-radius: 6px;
  185 + cursor: pointer;
  186 + font-size: 0.8rem;
  187 + font-weight: 700;
  188 + display: flex;
  189 + justify-items: center;
  190 + align-items: center;
  191 + justify-content: center;
  192 + }
46 193  
47   - .btn-sync { background: rgba(34, 197, 94, 0.1); border: 1px dashed #22c55e; color: #22c55e; padding: 8px; margin: 15px; border-radius: 6px; cursor: pointer; font-size: 0.8rem; font-weight: 700; display: flex; justify-items: center; align-items: center; justify-content: center; }
48   - .btn-sync:hover { background: #22c55e; color: white; }
  194 + .btn-sync:hover {
  195 + background: #22c55e;
  196 + color: white;
  197 + }
49 198 </style>
50 199 </head>
  200 +
51 201 <body class="hold-transition sidebar-mini layout-fixed">
52 202 <div class="wrapper">
53 203 <!-- Header -->
54 204 <nav class="main-header navbar navbar-expand navbar-dark">
55 205 <ul class="navbar-nav">
56   - <li class="nav-item"><a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a></li>
  206 + <li class="nav-item"><a class="nav-link" data-widget="pushmenu" href="#" role="button"><i
  207 + class="fas fa-bars"></i></a></li>
57 208 <li class="nav-item d-none d-sm-inline-block">
58   - <span class="nav-link text-white font-weight-bold">SIGEM GIS | <small class="text-gray">Bienvenido, OPERADOR SISTEMA</small></span>
  209 + <span class="nav-link text-white font-weight-bold">SIGEM GIS | <small class="text-gray">Bienvenido,
  210 + OPERADOR SISTEMA</small></span>
59 211 </li>
60 212 </ul>
61 213 <ul class="navbar-nav ml-auto">
62   - <li class="nav-item"><a href="login.html" class="btn btn-sm btn-outline-danger mr-3 font-weight-bold">Cerrar Sesión</a></li>
  214 + <li class="nav-item"><a href="login.html"
  215 + class="btn btn-sm btn-outline-danger mr-3 font-weight-bold">Cerrar Sesión</a></li>
63 216 </ul>
64 217 </nav>
65 218  
... ... @@ -99,7 +252,8 @@
99 252 <i class="fas fa-chart-pie"></i> Por Total (Percentiles)
100 253 </div>
101 254 <div class="btn-map-mode" id="btn-png" onclick="setMapMode('png')">
102   - <i class="fas fa-image text-warning"></i> Vista PNG (Full) <small class="ml-1 text-muted">[M]</small>
  255 + <i class="fas fa-image text-warning"></i> Vista PNG (Full) <small
  256 + class="ml-1 text-muted">[M]</small>
103 257 </div>
104 258  
105 259 <div class="map-section-title">Administración</div>
... ... @@ -114,116 +268,128 @@
114 268 <div class="content-wrapper">
115 269 <div id="map"></div>
116 270 <div class="top-widget" id="top-widget">📊 MOROSIDAD - ÚLTIMO ADEUDADO</div>
117   -
  271 +
118 272 <div class="legend-card" id="legend">
119 273 <div class="legend-title">Último Adeudado</div>
120 274 <div class="legend-item"><span class="legend-color" style="background:#71de75;"></span> 🟢 <= 2026</div>
121   - <div class="legend-item"><span class="legend-color" style="background:#bef264;"></span> 🎾 <= 2025</div>
122   - <div class="legend-item"><span class="legend-color" style="background:#ffaa00;"></span> 🟡 <= 2024</div>
123   - <div class="legend-item"><span class="legend-color" style="background:#fb923c;"></span> 🟠 <= 2023</div>
124   - <div class="legend-item"><span class="legend-color" style="background:#f87171;"></span> 🔴 <= 2022</div>
125   - <div class="legend-item"><span class="legend-color" style="background:#e11d48;"></span> 📢 <= 2021</div>
126   - <div class="legend-item"><span class="legend-color" style="background:#475569;"></span> 🔘 SIN DEUDA / PRESCRIPTAS</div>
127   - <div class="mt-2 pt-2 border-top border-secondary text-muted" style="font-size:0.6rem">Fuente: Sistema SIGEM</div>
128   - </div>
129   - </div>
130   - </div>
131   -
132   - <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
133   - <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.1/js/bootstrap.bundle.min.js"></script>
134   - <script src="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.2.0/js/adminlte.min.js"></script>
135   - <script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
136   -
137   - <script>
138   - const entidad = localStorage.getItem('entidad') || '505';
139   - const contextPath = '/gis-geoserver';
140   - let map;
141   -
142   - async function init() {
143   - try {
144   - const r = await fetch(`${contextPath}/api/gis/entidad/${entidad}`);
145   - const data = await r.json();
146   - const res = await fetch(`${contextPath}/api/gis/entidad/${entidad}/resumen`);
147   - const stats = await res.json();
148   -
149   - document.getElementById('val-total').textContent = stats.total_lotes.toLocaleString();
150   - document.getElementById('val-morosos').textContent = stats.lotes_con_deuda.toLocaleString();
151   -
152   - const center = data.latlong.split(',').map(Number);
153   -
154   - map = new maplibregl.Map({
155   - container: 'map',
156   - style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
157   - center: [center[1], center[0]],
158   - zoom: parseInt(data.zoom) || 15,
159   - pitch: 0,
160   - bearing: 0,
161   - dragRotate: false
162   - });
163   -
164   - map.on('load', () => {
165   - // Fuente Vectorial (MVT) compatible con Regla 23
166   - map.addSource('lotes-mvt', {
167   - type: 'vector',
168   - tiles: [ `${window.location.origin}${contextPath}/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=sigem:vw_lotes_morosidad_${entidad}&STYLE=&TILEMATRIXSET=GoogleMapsCompatible&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=application/x-protobuf` ],
169   - scheme: 'xyz'
170   - });
171   -
172   - map.addLayer({
173   - id: 'lotes-layer',
174   - type: 'line',
175   - source: 'lotes-mvt',
176   - 'source-layer': `vw_lotes_morosidad_${entidad}`,
177   - paint: { 'line-color': '#475569', 'line-width': 0.8 }
178   - });
179   -
180   - // Fuente PNG (WMS) - Alineada a Regla 5 (EPSG:4326)
181   - map.addSource('lotes-wms', {
182   - type: 'raster',
183   - tiles: [ `${window.location.origin}${contextPath}/geoserver/wms?service=WMS&version=1.1.1&request=GetMap&layers=sigem:vw_lotes_wms_${entidad}&bbox={bbox-epsg-4326}&width=256&height=256&srs=EPSG:4326&styles=morosidad_style_wms&format=image/png&transparent=true` ],
184   - tileSize: 256
185   - });
186   -
187   - map.addLayer({
188   - id: 'lotes-color-wms',
189   - type: 'raster',
190   - source: 'lotes-wms',
191   - paint: { 'raster-opacity': 0 }
192   - }, 'lotes-layer');
193   - });
194   -
195   - map.on('click', 'lotes-layer', (e) => {
196   - const p = e.features[0].properties;
197   - const content = `<div class="p-2"><b class="text-primary">CCC: ${p.ccc || 'N/A'}</b><hr class="my-1">Deuda: Gs. ${Number(p.trb_total_deuda || 0).toLocaleString()}<br>Último Pago: ${p.ultimo_pago || 'Nunca'}</div>`;
198   - new maplibregl.Popup().setLngLat(e.lngLat).setHTML(content).addTo(map);
199   - });
200   -
201   - } catch (e) {
202   - console.error("Error cargando dashboard:", e);
203   - // Si hay 503, re-intentar en 5s
204   - setTimeout(init, 5000);
205   - }
206   - }
207   -
208   - function setMapMode(mode) {
209   - $('.btn-map-mode').removeClass('active');
210   - const legend = document.getElementById('legend');
211   - const topWidget = document.getElementById('top-widget');
212   -
213   - if (mode === 'base' || mode === 'general') {
214   - $('#btn-base').addClass('active');
215   - map.setPaintProperty('lotes-color-wms', 'raster-opacity', 0);
216   - legend.style.display = 'none';
217   - topWidget.style.display = 'none';
218   - } else {
219   - $(`#btn-${mode}`).addClass('active');
220   - map.setPaintProperty('lotes-color-wms', 'raster-opacity', 0.8);
221   - legend.style.display = 'block';
222   - topWidget.style.display = 'block';
223   - }
224   - }
225   -
226   - init();
227   - </script>
  275 + <div class="legend-item"><span class="legend-color" style="background:#bef264;"></span> 🎾 <=
  276 + 2025</div>
  277 + <div class="legend-item"><span class="legend-color" style="background:#ffaa00;"></span>
  278 + 🟡 <= 2024</div>
  279 + <div class="legend-item"><span class="legend-color"
  280 + style="background:#fb923c;"></span> 🟠 <= 2023</div>
  281 + <div class="legend-item"><span class="legend-color"
  282 + style="background:#f87171;"></span> 🔴 <= 2022</div>
  283 + <div class="legend-item"><span class="legend-color"
  284 + style="background:#e11d48;"></span> 📢 <= 2021</div>
  285 + <div class="legend-item"><span class="legend-color"
  286 + style="background:#475569;"></span> 🔘 SIN DEUDA
  287 + / PRESCRIPTAS</div>
  288 + <div class="mt-2 pt-2 border-top border-secondary text-muted"
  289 + style="font-size:0.6rem">Fuente: Sistema SIGEM</div>
  290 + </div>
  291 + </div>
  292 + </div>
  293 +
  294 + <script
  295 + src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  296 + <script
  297 + src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.1/js/bootstrap.bundle.min.js"></script>
  298 + <script
  299 + src="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.2.0/js/adminlte.min.js"></script>
  300 + <script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
  301 +
  302 + <script>
  303 + const entidad = localStorage.getItem('entidad') || '505';
  304 + const contextPath = '/gis-geoserver';
  305 + let map;
  306 +
  307 + async function init() {
  308 + try {
  309 + const r = await fetch(`${contextPath}/api/gis/entidad/${entidad}`);
  310 + const data = await r.json();
  311 + const res = await fetch(`${contextPath}/api/gis/entidad/${entidad}/resumen`);
  312 + const stats = await res.json();
  313 +
  314 + document.getElementById('val-total').textContent = stats.total_lotes.toLocaleString();
  315 + document.getElementById('val-morosos').textContent = stats.lotes_con_deuda.toLocaleString();
  316 +
  317 + const center = data.latlong.split(',').map(Number);
  318 +
  319 + map = new maplibregl.Map({
  320 + container: 'map',
  321 + style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
  322 + center: [center[1], center[0]],
  323 + zoom: parseInt(data.zoom) || 15,
  324 + pitch: 0,
  325 + bearing: 0,
  326 + dragRotate: false
  327 + });
  328 +
  329 + map.on('load', () => {
  330 + // Fuente Vectorial (MVT) compatible con Regla 23
  331 + map.addSource('lotes-mvt', {
  332 + type: 'vector',
  333 + tiles: [`http://192.168.1.123:8083/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=sigem:e${entidad}_lotes_activos&STYLE=&TILEMATRIXSET=GoogleMapsCompatible&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=application/x-protobuf`],
  334 + scheme: 'xyz'
  335 + });
  336 +
  337 + map.addLayer({
  338 + id: 'lotes-layer',
  339 + type: 'line',
  340 + source: 'lotes-mvt',
  341 + 'source-layer': `e${entidad}_lotes_activos`,
  342 + paint: { 'line-color': '#475569', 'line-width': 0.8 }
  343 + });
  344 +
  345 + // Fuente PNG (WMS) - Alineada a Regla 5 (EPSG:4326)
  346 + map.addSource('lotes-wms', {
  347 + type: 'raster',
  348 + tiles: [ `http://192.168.1.123:8083/geoserver/sigem/wms?service=WMS&version=1.1.1&request=GetMap&layers=sigem:vw_lotes_morosidad_${entidad}&bbox={bbox-epsg-3857}&width=256&height=256&srs=EPSG:3857&styles=&format=image/png&transparent=true` ],
  349 + tileSize: 256
  350 + });
  351 +
  352 + map.addLayer({
  353 + id: 'lotes-color-wms',
  354 + type: 'raster',
  355 + source: 'lotes-wms',
  356 + paint: { 'raster-opacity': 0 }
  357 + }, 'lotes-layer');
  358 + });
  359 +
  360 + map.on('click', 'lotes-layer', (e) => {
  361 + const p = e.features[0].properties;
  362 + const content = `<div class="p-2"><b class="text-primary">CCC: ${p.ccc || 'N/A'}</b><hr class="my-1">Deuda: Gs. ${Number(p.trb_total_deuda || 0).toLocaleString()}<br>Último Pago: ${p.ultimo_pago || 'Nunca'}</div>`;
  363 + new maplibregl.Popup().setLngLat(e.lngLat).setHTML(content).addTo(map);
  364 + });
  365 +
  366 + } catch (e) {
  367 + console.error("Error cargando dashboard:", e);
  368 + // Si hay 503, re-intentar en 5s
  369 + setTimeout(init, 5000);
  370 + }
  371 + }
  372 +
  373 + function setMapMode(mode) {
  374 + $('.btn-map-mode').removeClass('active');
  375 + const legend = document.getElementById('legend');
  376 + const topWidget = document.getElementById('top-widget');
  377 +
  378 + if (mode === 'base' || mode === 'general') {
  379 + $('#btn-base').addClass('active');
  380 + map.setPaintProperty('lotes-color-wms', 'raster-opacity', 0);
  381 + legend.style.display = 'none';
  382 + topWidget.style.display = 'none';
  383 + } else {
  384 + $(`#btn-${mode}`).addClass('active');
  385 + map.setPaintProperty('lotes-color-wms', 'raster-opacity', 0.8);
  386 + legend.style.display = 'block';
  387 + topWidget.style.display = 'block';
  388 + }
  389 + }
  390 +
  391 + init();
  392 + </script>
228 393 </body>
229   -</html>
  394 +
  395 +</html>
230 396 \ No newline at end of file
... ...
src/main/resources/static/mapas.html
... ... @@ -437,7 +437,7 @@
437 437 map.addSource('lotes-mvt', {
438 438 type: 'vector',
439 439 tiles: [
440   - `${window.location.origin}/gis-geoserver/gwc/service/tms/1.0.0/sigem:vw_lotes_morosidad_${entidad}@XYZ-900913@pbf/{z}/{x}/{y}.pbf`
  440 + `${window.location.origin}/geoserver/gwc/service/tms/1.0.0/sigem:e${entidad}_lotes_activos@XYZ-900913@pbf/{z}/{x}/{y}.pbf`
441 441 ],
442 442 scheme: 'tms'
443 443 });
... ... @@ -449,7 +449,7 @@
449 449 id: 'lotes-layer',
450 450 type: 'fill',
451 451 source: 'lotes-mvt',
452   - 'source-layer': `vw_lotes_morosidad_${entidad}`,
  452 + 'source-layer': `e${entidad}_lotes_activos`,
453 453 paint: {
454 454 'fill-color': 'rgba(59, 130, 246, 0.1)', // Azul tenue inicial
455 455 'fill-outline-color': 'rgba(255, 255, 255, 0.3)' // Borde blanco visible
... ... @@ -457,23 +457,13 @@
457 457 });
458 458 }
459 459  
460   - // Fuente de Mejoras (MVT) - TMS Nativo de GeoServer
461   - if (!map.getSource('mejoras-mvt')) {
462   - map.addSource('mejoras-mvt', {
463   - type: 'vector',
464   - tiles: [
465   - `${window.location.origin}/gis-geoserver/gwc/service/tms/1.0.0/sigem:e${entidad}_mejoras@XYZ-900913@pbf/{z}/{x}/{y}.pbf`
466   - ],
467   - scheme: 'tms'
468   - });
469   - }
470 460  
471 461 // [PRUEBA CONTROLADA] Fuente Raster WMS (Renderizado en Servidor)
472 462 if (!map.getSource('lotes-wms')) {
473 463 map.addSource('lotes-wms', {
474 464 'type': 'raster',
475 465 'tiles': [
476   - `${window.location.origin}/gis-geoserver/sigem/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=sigem:vw_lotes_morosidad_${entidad}&STYLES=morosidad_style_wms&FORMAT=image/png&TRANSPARENT=TRUE&WIDTH=256&HEIGHT=256&SRS=EPSG:3857&BBOX={bbox-epsg-3857}`
  466 + `${window.location.origin}/geoserver/sigem/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=sigem:vw_lotes_morosidad_${entidad}&STYLES=morosidad_style_wms&FORMAT=image/png&TRANSPARENT=TRUE&WIDTH=256&HEIGHT=256&SRS=EPSG:3857&BBOX={bbox-epsg-3857}`
477 467 ],
478 468 'tileSize': 256
479 469 });
... ... @@ -489,19 +479,7 @@
489 479 }); // Agregamos al final
490 480 }
491 481  
492   - // Capa de Mejoras (Inicialmente oculta o 2D)
493   - if (!map.getLayer('mejoras-layer')) {
494   - map.addLayer({
495   - id: 'mejoras-layer',
496   - type: 'fill',
497   - source: 'mejoras-mvt',
498   - 'source-layer': `e${entidad}_mejoras`,
499   - paint: {
500   - 'fill-color': 'rgba(59, 130, 246, 0.2)',
501   - 'fill-outline-color': 'rgba(59, 130, 246, 0.5)'
502   - }
503   - });
504   - }
  482 +
505 483 }
506 484  
507 485 // --- Lógica de Capas Base (Estricto Fondo Nativo) ---
... ...
GitLab Appliance - Powered by TurnKey Linux