# Laboratorio de referencia Open GIS de GeoSAT

Ejercicio reproducible de migración en español e inglés. Parte de un conjunto de datos, conserva sus identificadores, publícalo mediante contratos de servicio explícitos y registra lo que realmente funciona. Todo el contenido de `data/` es sintético: la cuadrícula se ubica cerca de Medellín como referencia geográfica, pero no representa activos municipales reales. No es un caso de cliente, ahorro de migración observado, comparación de rendimiento con Esri ni estimación de capacidad de producción.

## Contenido

| Archivo | Propósito |
|---|---|
| `data/municipal-assets.csv` | 10.000 puntos generados con longitud y latitud explícitas |
| `data/municipal-assets.geojson` | Los mismos registros en GeoJSON RFC 7946 |
| `data/municipal-assets.gpkg` | GeoPackage generado con GDAL, capa `assets` e índice espacial |
| `data/assets-shapefile.zip` | Datos opcionales para GeoServer nativo; `inspected_on` se convierte en `inspect_dt` por el límite de nombres DBF |
| `qgis/municipal-assets.qgs` | Proyecto con ruta relativa al GeoPackage y formulario de valores para estado |
| `sql/01-init.sql` | Esquema PostGIS, restricciones, importación, índices, lector y editor restringido |
| `sql/02-acceptance.sql` | Conteos, SRID, geometría, filtro espacial y privilegios |
| `sql/03-export.sql` | Exportación GeoJSON con identificadores estables |
| `geoserver/assets.sld` | Estilo de puntos good/inspect/repair |
| `pygeoapi/` | Configuraciones CSV y PostGIS, versiones de dependencias y contenedor |
| `viewer/` | Visor MapLibre local con JS/CSS incluidos, sin mapa base de pago ni clave API |
| `worksheets/` | Inventario de migración, costos y hoja de evidencias de aceptación |
| `measurements/` | Resultados locales reales, tiempos individuales, método y limitaciones |

## Alcance de la verificación

Ejecutado para esta entrega: paridad CSV/GeoJSON; conversión GeoJSON → GeoPackage y lectura de regreso con pyogrio 0.11.1 / GDAL 3.10.3; contrato HTTP y tiempos de pygeoapi 0.24.0 con proveedor CSV; GeoServer 2.28.5 nativo en Java 17.0.20 con almacén Shapefile, representación WMS, consultas WFS y rechazo de administración REST anónima. Todas las solicitudes cronometradas de las corridas publicadas devolvieron 200.

No ejecutado aquí: Docker Compose, SQL PostGIS, almacén PostGIS de GeoServer, proveedor PostgreSQL de pygeoapi, interfaz QGIS, sincronización QField/dispositivos, identidad federada, restauración de respaldo, carga concurrente y despliegue productivo. Se entregan ejemplos completos y comandos de aceptación; no se presentan como pruebas ya superadas. Consultar los manifiestos del registro para fijar imágenes no demuestra que los contenedores arrancaron.

## 1. Inspeccionar y validar los datos (Python 3.11+)

Descomprime el ZIP y abre una terminal dentro de `open-gis-lab`. Los comandos usan una shell POSIX; en Windows puedes usar WSL2. Conserva las rutas. No uses la única copia de un conjunto de datos real para practicar.

```sh
python3 scripts/check-data.py
```

Resultado esperado: `PASS`, 10.000 identificadores únicos, paridad de coordenadas CSV/GeoJSON, 1.000 registros del distrito D01 y 441 en la envolvente de prueba. Los identificadores van de `asset-00001` a `asset-10000`. El orden es longitud, latitud; la extensión es `[-75.65, 6.15, -75.551, 6.249]`.

`asset_type` contiene hydrant/streetlight/drain; `condition`, good/inspect/repair; `district`, D01–D10; `inspected_on`, una fecha ISO. Los patrones de atributos se correlacionan deliberadamente para facilitar comprobaciones. No describen infraestructura real. Para distancias o áreas en metros usa una proyección adecuada o una operación geography; los grados no son metros.

Para regenerar CSV y GeoJSON ejecuta `python3 scripts/generate-data.py`: sobrescribe los archivos sintéticos. Tras instalar las herramientas del siguiente paso, `python scripts/make-geopackage.py` y `python scripts/make-shapefile.py` regeneran sus archivos respectivos. La conversión GeoPackage comprueba geometrías y atributos después de escribir.

## 2. Ejecutar una OGC API sin Docker

Usa un entorno aislado. No requiere instalar un SIG globalmente. Las dependencias de herramientas incluyen la regeneración opcional de formatos. Descargar paquetes necesita internet; servir los datos locales no.

```sh
python3 -m venv .local/venv
. .local/venv/bin/activate
python -m pip install -r pygeoapi/requirements-tools.txt
export PYGEOAPI_CONFIG="$PWD/pygeoapi/config-csv.yml"
export PYGEOAPI_OPENAPI="$PWD/.local/openapi.yml"
pygeoapi openapi generate "$PYGEOAPI_CONFIG" > "$PYGEOAPI_OPENAPI"
pygeoapi serve
```

Deja abierta esa terminal. En otra:

```sh
curl --fail 'http://localhost:5000/collections?f=json'
curl --fail 'http://localhost:5000/collections/assets/items?f=json&limit=10'
curl --fail 'http://localhost:5000/collections/assets/items?f=json&limit=10&offset=10'
curl --fail 'http://localhost:5000/collections/assets/items?f=json&district=D01&limit=10'
curl --fail 'http://localhost:5000/collections/assets/items?f=json&bbox=-75.63,6.17,-75.61,6.19&limit=100'
curl --fail 'http://localhost:5000/collections/assets/items/asset-10000?f=json'
```

Verifica `numberMatched=10000` sin filtro; la segunda página comienza en `asset-00011`; D01 coincide con 1.000 registros; la envolvente coincide con 441 y devuelve máximo 100. Un ID ausente responde 404. `limit=not-a-number` responde 400. El límite configurado de 100 se aplica a cada respuesta, no al tamaño total de la colección. Usa enlaces `next` o paginación por offset para las páginas restantes. No presupongas que todos los proveedores implementan los mismos filtros, ordenamiento, transacciones o transformaciones CRS.

La API CSV es de lectura. CORS está habilitado para que el visor local, servido en otro puerto, pueda consultarla. El ejemplo no configura autenticación, proxy público, limitación de solicitudes ni una lista CORS productiva.

## 3. Abrir el mapa web y el proyecto QGIS

Desde la raíz del kit, en otra terminal:

```sh
python3 -m http.server 8850 --bind 127.0.0.1
```

Abre `http://localhost:8850/viewer/`. Por defecto carga los 10.000 puntos GeoJSON; el botón API muestra solo los primeros 100, para hacer visible la paginación. Cambia entre inglés y español y pulsa un punto para ver atributos. Verde, ámbar y rojo indican good, inspect y repair. No usa teselas externas. El fondo vacío es intencional: el ejercicio evalúa datos e identificadores.

En QGIS 3.x abre `qgis/municipal-assets.qgs`. Referencia `../data/municipal-assets.gpkg|layername=assets`. Si tu versión ajusta opciones del proyecto, guarda una copia y registra la versión exacta. Comprueba 10.000 elementos, EPSG:4326 e identificadores estables. Abre el formulario: `asset_id` debe estar bloqueado y `condition` ofrecer los tres valores. Edita una COPIA del GeoPackage y verifica persistencia al reabrirlo. El XML se comprobó estructuralmente, pero esta entrega no abrió QGIS; esta lista define tu aceptación de escritorio.

Para QField, el kit ofrece datos y proyecto iniciales, no un despliegue de campo ya empaquetado y sincronizado. Valida primero formularios QGIS y luego empaqueta una copia con el flujo correspondiente a tu versión. Prueba modo avión, cambios simultáneos sobre el mismo registro en dos dispositivos, rutas de adjuntos, reconexión, estabilidad de IDs y reversión antes de aprobar el flujo productivo.

## 4. Ejecutar la plataforma opcional PostGIS + GeoServer + pygeoapi

Requiere Docker Engine/Desktop, Compose v2, memoria disponible para PostgreSQL y Java, e internet para imágenes y dependencias. PostGIS y GeoServer están fijados a Linux amd64; equipos ARM pueden necesitar emulación. Sus tiempos no son comparables con la ejecución nativa incluida. Todos los puertos publicados se restringen a `127.0.0.1`.

Genera secretos desechables distintos en la shell. No se escribe `.env` ni se incluyen credenciales reales:

```sh
export LAB_DB_PASSWORD="$(python3 -c 'import secrets; print(secrets.token_urlsafe(24))')"
export LAB_READER_PASSWORD="$(python3 -c 'import secrets; print(secrets.token_urlsafe(24))')"
export LAB_EDITOR_PASSWORD="$(python3 -c 'import secrets; print(secrets.token_urlsafe(24))')"
export LAB_GEOSERVER_PASSWORD="$(python3 -c 'import secrets; print(secrets.token_urlsafe(24))')"
docker compose config --quiet
docker compose up -d --build
docker compose ps
```

Detén primero la API CSV nativa si usa el puerto 5000. La inicialización de PostgreSQL solo corre en un volumen vacío. Conserva esta shell para publicar: generar nuevas variables no cambia la contraseña de una base ya creada. Compose indica las variables faltantes. No publiques la configuración expandida ni volcados de variables del contenedor: contienen esos secretos desechables.

Conexiones: PostgreSQL `127.0.0.1:55432`, base `geosat_lab`; GeoServer `http://localhost:8080/geoserver`; OGC API `http://localhost:5000`. El propietario es `lab_owner`; GeoServer y la API usan `lab_reader`. `lab_editor` solo puede actualizar `condition` e `inspected_on`, no geometría ni inserciones/borrados. Demuestra privilegios; no implementa un sistema completo de edición multiusuario ni resolución de conflictos.

Ejecuta aceptación de base de datos:

```sh
docker compose exec -T db psql -U lab_owner -d geosat_lab < sql/02-acceptance.sql
```

Espera 10.000 registros, puntos válidos EPSG:4326, D01=1.000, envolvente=441, plan de consulta y privilegios `true,false,true,false`. El planificador decide si usa un índice: lee el plan antes de afirmarlo. La actualización del editor ocurre en una transacción que se revierte.

Cuando GeoServer responda, publica el almacén PostGIS y el estilo:

```sh
. .local/venv/bin/activate
python scripts/publish-geoserver.py --mode postgis
```

El publicador exige contraseñas explícitas por entorno y restringe el host GeoServer a loopback. Crea espacio `lab`, almacén `assets`, capa `lab:assets` y estilo por estado. Reutiliza nombres existentes; si antes publicaste Shapefile con el mismo nombre, usa un volumen nuevo de laboratorio. No configura SSO, transacciones WFS, alojamiento público ni emulación de ArcGIS REST.

Comprueba servicios:

```sh
curl --fail 'http://localhost:8080/geoserver/lab/wms?service=WMS&request=GetCapabilities'
curl --fail 'http://localhost:8080/geoserver/lab/wfs?service=WFS&version=2.0.0&request=GetFeature&typeNames=lab:assets&outputFormat=application/json&srsName=EPSG:4326&count=10'
curl --fail 'http://localhost:5000/collections/assets/items?f=json&limit=10'
docker compose exec -T db psql -U lab_owner -d geosat_lab < sql/03-export.sql > .local/export.geojson
```

En WMS 1.1.1 el ejemplo usa longitud/latitud con `SRS=EPSG:4326`; WMS 1.3.0 puede respetar el orden de ejes definido por el CRS. Copiar BBOX entre versiones puede producir un mapa vacío. Un WFS o una colección OGC API tampoco convierte en compatible una aplicación que exige `/FeatureServer` o `/MapServer`: verifica cada consumidor real.

## 5. Alternativa GeoServer nativo y mediciones reales

La corrida publicada usó el binario oficial 2.28.5, Java 17.0.20, `-Xms128m -Xmx768m` y loopback. El binario no está incluido. Descárgalo de la página oficial, usa un directorio de datos desechable, fija `jetty.http.host=127.0.0.1` y puerto 8080. Sigue la instalación oficial para configurar una contraseña de administrador propia; exporta `GEOSERVER_USER` y `LAB_GEOSERVER_PASSWORD`. El kit no incluye ni intenta descubrir contraseñas.

```sh
python scripts/make-shapefile.py
python scripts/publish-geoserver.py --mode shapefile
python scripts/measure-geoserver.py --store shapefile
python scripts/measure-pygeoapi.py
```

`measure-pygeoapi.py` abre y cierra su propio servidor HTTP local en un puerto disponible. `measure-geoserver.py` consulta el GeoServer que abriste. Cada comando reemplaza sus archivos de resultados en `measurements/`; conserva una copia de los originales para comparar tu equipo.

La corrida pygeoapi incluye 11 comprobaciones y cuatro cargas de 30 solicitudes medidas, tras cinco calentamientos por carga. GeoServer incluye ocho comprobaciones y tres cargas de 30 solicitudes, también con cinco calentamientos. Ambas usan un cliente secuencial, tiempos de respuesta completa, sin TLS y sin vaciar cachés. p50 es la mediana; p95 usa rango más próximo. Se incluyen muestras individuales, tamaños y hashes del conjunto. El PNG es una respuesta GetMap real.

Un filtro espacial sobre CSV necesita examinar registros; el tamaño de página no explica todo el costo. Representar WMS y recuperar elementos WFS/OGC API son salidas distintas. Ninguna observación determina qué producto es más rápido. Estas mediciones no estiman concurrencia productiva, latencia cloud, recuperación ni ahorro.

## 6. Practicar respaldo y restauración en la plataforma desechable

Este procedimiento opcional no se ejecutó para la entrega publicada. Crea otra base dentro del contenedor y conserva el original:

```sh
mkdir -p .local/backups
docker compose exec -T db pg_dump -U lab_owner -d geosat_lab --format=custom > .local/backups/geosat-lab.dump
docker compose exec -T db createdb -U lab_owner geosat_restore
docker compose exec -T db pg_restore -U lab_owner -d geosat_restore --no-owner < .local/backups/geosat-lab.dump
docker compose exec -T db psql -U lab_owner -d geosat_restore < sql/02-acceptance.sql
```

Compara conteos, conjuntos de IDs, hashes geométricos y atributos. Mide el tiempo de restauración y dónde está la copia: un archivo en el mismo disco que falló no resuelve recuperación. Los roles son del clúster y ya existen en este laboratorio; otro servidor requiere un procedimiento explícito de roles y propiedad.

`docker compose stop` detiene conservando datos. `docker compose down` elimina contenedores/red, pero conserva volúmenes. El comando separado `docker compose down -v` borra los volúmenes de base y GeoServer de este laboratorio: úsalo solo cuando quieras descartar esos datos desechables. No sustituyas un proyecto Compose productivo.

## 7. Convertir el ejercicio en una decisión de migración

Completa `worksheets/migration-audit.csv` con flujos reales y responsables. Inventaría URLs, aplicaciones, scripts, identidad, dispositivos, adjuntos, transformaciones CRS, licencias, recuperación y soporte. Conserva un componente ArcGIS cuando una tarea obligatoria no supere su prueba de reemplazo.

Completa `worksheets/tco-inputs.csv` con cotizaciones, horas y supuestos documentados, una moneda y un horizonte. El costo anual actual suma licencias, infraestructura, soporte y trabajo interno. El costo futuro usa los mismos rubros y añade migración, formación y operación paralela iniciales. Ahorro no descontado = anual actual × años − anual futuro × años − transición. Recuperación simple en meses = transición / (anual actual − anual futuro) × 12, solo si la diferencia anual es positiva. Un ahorro negativo o recuperación fuera del horizonte es válido. Una celda vacía es información faltante, no cero. No hay precios de fabricante precargados ni conversión monetaria automática.

En `worksheets/acceptance.csv` adjunta resultados propios; no marques como superadas las pruebas no ejecutadas del kit. Añade polígonos, adjuntos, relaciones, edición versionada o datos propios sanitizados después de documentar derechos y requisitos de compatibilidad.

## Resolución de problemas

| Síntoma | Comprobación |
|---|---|
| La API no encuentra CSV | Ejecuta desde la raíz; la ruta del dato es relativa |
| Puerto ocupado | Detén el proceso nativo anterior; no publiques en todas las interfaces |
| Falta CORS | Instala los requisitos, incluido Flask-CORS |
| QGIS no encuentra capa | Conserva rutas relativas y selecciona `assets` del GeoPackage |
| Capa GeoServer falla | Comprueba base, tipo de almacén, contraseña lectora, esquema y tabla |
| El SQL inicial no cambia datos | Solo corre una vez en un volumen vacío |
| API devuelve únicamente 100 | Es el máximo por página; pagina |
| WMS aparece vacío | Comprueba capa, estilo, extensión, versión y ejes |
| Fallan privilegios | Confirma base desechable y roles correctos |
| Inicio lento en ARM | Las imágenes amd64 pueden usar emulación |
| Falla formulario o sincronización | Registra versión QGIS/QField y tarea; disponibilidad del servidor no prueba el trabajo de campo |

## Fuentes y versiones

- [Documentación pygeoapi 0.24 y capacidades de proveedores](https://docs.pygeoapi.io/en/stable/publishing/ogcapi-features.html)
- [GeoServer 2.28.5](https://geoserver.org/release/2.28.5/)
- [Contenedor oficial GeoServer](https://github.com/geoserver/docker)
- [Contenedor oficial PostGIS](https://github.com/postgis/docker-postgis)
- [Documentación PostGIS](https://postgis.net/documentation/)
- [Manual QGIS](https://docs.qgis.org/latest/en/docs/user_manual/)
- [Documentación QField](https://docs.qfield.org/)
- [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/)
- [Estándar GeoPackage](https://www.geopackage.org/)

Los manifiestos están en `measurements/container-images.json`; Compose y la imagen base Python usan digests inmutables. Las dependencias Python directas tienen versiones fijas; la resolución transitiva puede cambiar entre instalaciones. Cada resultado JSON declara el entorno realmente medido, diferente de la base Python opcional del contenedor. Al convertir el ejemplo en un sistema mantenido, comprueba avisos de seguridad y versiones soportadas.
