Outro dia eu estava navegando pela internet quando “esbarrei” com um post muito interessante sobre como utilizar o WFS Transactions (WFS-T), para salvar feições no PostGIS a partir de uma aplicação web com OpenLayers.

Eu já tinha postado algo bem similar a isso a um bom tempo atrás, mas já estava bastante desatualizado, então eu decidir traduzir e (re)postar esse material aqui no blog.

1. O Projeto

Essa aplicação está postada no GitHub e usa no frontend React/OpenLayers para atualizar dados de recursos GIS armazenados em um banco de dados PostGIS usando transações WFS (facilitadas pelo GeoServer).

2. Objetivo

O objetivo era exibir um recurso WFS em um mapa com OpenLayers e gravar alguns dados no PostGIS cada vez que o recurso fosse clicado. Isso foi feito incluindo a propriedade interation nos dados do recurso que rastreou o número de cliques.

Foi utilizado o docker kartoza/docker-geoserver para montar o backend com GeoServer e PostGIS. Graças ao trabalho duro do Kartoza, isso foi tão fácil quanto executar docker-compose up no diretório apropriado (mais instruções aqui).

Alguma configuração foi necessária para criar uma tabela e um registro de exemplo no PostGIS. Uma vez que isso foi concluído, mais algumas etapas foram necessárias para criar um workspace, store e uma camada no GeoServer para publicar a tabela do PostGIS.

A etapa final é publicar a camada no GeoServer e aí começa a diversão!

3. O Frontend

O aplicativo frontend foi baseado em React com OpenLayers. Alguns call-outs específicos e lições aprendidas são compartilhados abaixo, mas confira o projeto no GitHub para o código-fonte completo.

3.1 Criando a camada WFS do GeoServer no OpenLayers

Definir a camada e os estilos do WFS foi simples usando a estratégia bbox padrão, usada para instruir o OpenLayers sobre como/quando carregar os recursos do WFS. Veja:

import VectorSource from 'ol/source/Vector';
import GeoJSON from 'ol/format/GeoJSON.js';
import {bbox as bboxStrategy} from 'ol/loadingstrategy.js';
import VectorLayer from 'ol/layer/Vector';

const GEOSERVER_BASE_URL = 'http://localhost:8600/geoserver/dev';

// create geoserver generic vector features layer
const featureSource = new VectorSource({
  format: new GeoJSON(),
  url: function (extent) {
    return (
      GEOSERVER_BASE_URL + '/ows?service=WFS&' +
      'version=1.0.0&request=GetFeature&typeName=dev%3Ageneric&maxFeatures=50&' + 
      'outputFormat=application%2Fjson&srsname=EPSG:3857&' +
      'bbox=' +
      extent.join(',') +
      ',EPSG:3857'
    );
  },
  strategy: bboxStrategy,
});

const featureLayer = new VectorLayer({
  source: featureSource,
  style: {
    'stroke-width': 0.75,
    'stroke-color': 'white',
    'fill-color': 'rgba(100,100,100,0.25)',
  },
});

3.2 Usando React Refs para acessar objetos OpenLayers

Ao integrar o OpenLayers com o React, é importante inicializar os objetos do OpenLayers uma vez (por exemplo, em um hook onload) e usar Refs para manter referências a esses objetos entre as renderizações.

Isso também permite que a versão atual desses objetos seja acessível em funções de retorno de chamada. Caso contrário, uma versão obsoleta do objeto pode ser fornecida ao retorno de chamada (capturada no momento do fechamento do retorno da chamada).

// react
import React, {  useEffect, useRef } from 'react';
import Map from 'ol/Map'

function MapWrapper(props) {

  // refs are used instead of state to allow integration with 3rd party map onclick callback;
  //  these are assigned at the end of the onload hook
  //  https://stackoverflow.com/a/60643670
  const mapRef = useRef();
  const mapElement = useRef();
  const featuresLayerRef = useRef();

  // other logic removed for brevity

  // react onload hook
  useEffect( () => {

    // create map
    const map = new Map({
      // config removed for brevity
    })

    // save map and featureLary references into React refs
    featuresLayerRef.current = featureLayer;
    mapRef.current = map

  },[])

  return (      
    <div>
      <div ref={mapElement} className="map-container"></div>
    </div>
  ) 

}

export default MapWrapper

No exemplo acima, os objetos OpenLayers map, featuresLayer e até mesmo o div mapElement são armazenados como Refs para uso em funções de retorno de chamada fora do React.

3.3 Executando transações WFS a partir de funções de retorno de chamada do OpenLayers

O ponto crucial de todo esse aplicativo é enviar as solicitações de transação WFS para o GeoServer com os dados de recurso do OpenLayers para gravar no PostGIS. Isso é tratado na função de retorno de chamada no onclick do mapa.

import WFS from 'ol/format/WFS';
import GML from 'ol/format/GML32';

const GEOSERVER_BASE_URL = 'http://localhost:8600/geoserver/dev';

// map click handler - uses state and refs available in closure
const handleMapClick = async (event) => {

  // get clicked feature from wfs layer
  // TODO: currently only handles a single feature
  const clickedCoord = mapRef.current.getCoordinateFromPixel(event.pixel);
  const clickedFeatures = featuresLayerRef.current.getSource().getFeaturesAtCoordinate(clickedCoord);
  if (!clickedFeatures.length) return; // exit callback if no features clicked
  const feature = clickedFeatures[0];

  // parse feature properties
  const featureData = JSON.parse(feature.getProperties()['data']);

  // iterate prop to test write-back
  if (featureData.iteration) {
    ++featureData.iteration;
  } else featureData.iteration = 1;

  // set property data back to feature
  feature.setProperties({ data: JSON.stringify(featureData) });
  console.log('clicked updated feature data', feature.getProperties())

  // prepare feature for WFS update transaction
  //  https://dbauszus.medium.com/wfs-t-with-openlayers-3-16-6fb6a820ac58
  const wfsFormatter = new WFS();
  const gmlFormatter = new GML({
    featureNS: GEOSERVER_BASE_URL,
    featureType: 'generic',
    srsName: 'EPSG:3857' // srs projection of map view
  });
  var xs = new XMLSerializer();
  const node = wfsFormatter.writeTransaction(null, [feature], null, gmlFormatter);
  var payload = xs.serializeToString(node);

  // execute POST
  await fetch(GEOSERVER_BASE_URL + '/wfs', {
    headers: new Headers({
      'Authorization': 'Basic ' + Buffer.from('admin:myawesomegeoserver').toString('base64'),
      'Content-Type': 'text/xml'
    }),
    method: 'POST',
    body: payload
  });

  // clear wfs layer features to force reload from backend to ensure latest properties
  //  are available
  featuresLayerRef.current.getSource().refresh();

  // display updated feature data on map
  setFeatureData(JSON.stringify(featureData));
}

O código acima é executado quando o recurso WFS é clicado. Isso aciona a seguinte lógica:

Linhas 11-14: o objeto de feição OpenLayers clicado é identificado
Linhas 17-25: a propriedade iteration do recurso é aumentada em 1 e salva de volta no recurso
Linhas 30-38: o recurso é convertido no formato apropriado para a transação WFS
Linhas 41-48: a solicitação de transação WFS é definida para a instância do Docker GeoServer criada anteriormente no projeto
Linhas 52-55: solicita que o OpenLayers recarregue os dados WFS do GeoServer para garantir que as propriedades de atualização estejam presentes

4. Para onde ir a partir daqui

Agora que temos um exemplo de como gravar dados GIS do OpenLayers no PostGIS, podemos expandir este aplicativo para suportar criação e edição de recursos mais complexos. Por exemplo, desenhar recursos com o OpenLayers.

Fonte: Taylor Callsen