Para interagir com o nosso algoritmo de roteamento vamos precisar de um cliente que possa realizar solicitações no padrão OGR para nossas camadas nearest_vertex e shortest_path no GeoServer.

Iremos implementar um cliente muito simples com este tutorial que vai deixar o usuário arrastar os marcadores para início e destino da rota e, em seguida, atualizar o mapa com uma linha indicando a rota mais curta entre os dois pontos. O cliente será escrito em OpenLayers 3 com JQuery.

route2

Nós estaremos usando o SDK Suíte para criar um modelo para a construção de nossa aplicação. Na linha de comando vamos executar o seguinte.

suite-sdk create routing ol3view

Agora nós teremos um diretório com a aplicação básica para visualizar as camadas. Existem vários arquivos neste diretório, mas só vamos nos preocupar com index.html e src/app/app.js.

Vamos primeiro precisar editar o arquivo index.html que carrega as bibliotecas OpenLayers e jQuery, para adicionar um componente onde possamos exibir informações sobre a rota. Encontre a linha que tem <div id = “map”> e adicione a seguinte linha antes: <div id = “info”></div>. Esta parte do arquivo agora deve ter a seguinte aparência:

  </div><!--/.navbar-collapse -->
</div>
<div id="info"></div>
<div id="map">
  <div id="popup" class="ol-popup">
  </div>
</div>

Agora, vamos construir nossa aplicação javascript passo-a-passo, mas ao contrário do que fizemos com o arquivo index.html vamos remover os arquivos app.js e escrever um novo a partir do zero. Não esqueça de colocar seus novos arquivos na pasta src/app.

Vamos começar declarando algumas variáveis, que incluirão o ponto inicial (center point) e o nível de zoom para o nosso mapa.

var geoserverUrl = '/geoserver';
var center = ol.proj.transform([-70.26, 43.67], 'EPSG:4326', 'EPSG:3857');
var zoom = 12;
var pointerDown = false;
var currentMarker = null;
var changed = false;
var routeLayer;
var routeSource;
var travelTime;
var travelDist;

Vamos precisar atualizar o texto em dois elementos no documento index.html como nossas mudanças de rota.

// elements in HTML document
var info = document.getElementById('info');
var popup = document.getElementById('popup');

Quando apresentarmos as informações sobre a nossa rota, teremos de formatar os dados para exibição. Por exemplo, o tempo que leva para viajar ao longo de uma rota é medido em horas, por isso vamos ter o número de 0,25 e formatá-lo para exibir 15 minutos. Faça alguma formatação de distâncias, nomes das estradas e cruzamentos.

Nosso mapa terá dois marcadores para que o usuário possa arrastá-los para novas posições e indicar o início e fim da rota.

markers

Nós vamos adicionar uma função para as camadas de sobreposição chamada changeHandler que será acionada sempre que um dos marcadores for movido.

// create a point with a colour and change handler
function createMarker(point, colour) {
  var marker = new ol.Feature({
    geometry: new ol.geom.Point(ol.proj.transform(point, 'EPSG:4326', 'EPSG:3857'))
  });
  marker.setStyle(
    [new ol.style.Style({
      image: new ol.style.Circle({
        radius: 6,
        fill: new ol.style.Fill({
          color: 'rgba(' + colour.join(',') + ', 1)'
        })
      })
    })]
  );
  marker.on('change', changeHandler);
  return marker;
}
var sourceMarker = createMarker([-70.26013, 43.66515], [0, 255, 0]);
var targetMarker = createMarker([-70.24667, 43.66996], [255, 0, 0]);
// create overlay to display the markers
var markerOverlay = new ol.FeatureOverlay({
  features: [sourceMarker, targetMarker],
});

A função para o movimento do marcador é muito simples: vamos manter um registro do marcador que quando se move indica que a rota foi alterada.

// record when we move one of the source/target markers on the map
function changeHandler(e) {
  if (pointerDown) {
    changed = true;
    currentMarker = e.target;
  }
}

Agora que os marcadores foram criados, podemos dizer ao OpenLayers que eles podem ser modificados pela interação do usuário:

var moveMarker = new ol.interaction.Modify({
  features: markerOverlay.getFeatures(),
  tolerance: 20
});

Vamos criar uma segunda camada, que será usada para exibir um pop-up quando o usuário clicar em segmentos da rota, e vamos destacar os segmentos selecionados com um estilo diferente.

// create overlay to show the popup box
var popupOverlay = new ol.Overlay({
  element: popup
});

// style routes differently when clicked
var selectSegment = new ol.interaction.Select({
  condition: ol.events.condition.click,
  style: new ol.style.Style({
      stroke: new ol.style.Stroke({
        color: 'rgba(255, 0, 128, 1)',
        width: 8
    })
  })
});

A camada base para a nossa aplicação será do OpenStreetMap, que é suportada pelo Openlayers 3. O mapa será criado com suporte para os marcadores e as diferentes interações que criamos sobre ele.

// set the starting view
var view = new ol.View({
  center: center,
  zoom: zoom
});
// create the map with OSM data
var map = new ol.Map({
  target: 'map',
  layers: [
    new ol.layer.Tile({
      source: new ol.source.OSM()
    })
  ],
  view: view,
  overlays: [popupOverlay, markerOverlay]
});
map.addInteraction(moveMarker);
map.addInteraction(selectSegment);
});

Vamos apresentar o pop-up quando o usuário clicar em um segmento de rota, mostrando o nome da estrada, a distância e o tempo necessário para atravessá-la.

// show pop up box when clicking on part of route
var getFeatureInfo = function(coordinate) {
  var pixel = map.getPixelFromCoordinate(coordinate);
  var feature = map.forEachFeatureAtPixel(pixel, function(feature, layer) {
    if (layer == routeLayer) {
      return feature;
    }
  });
  var text = null;
  if (feature) {
    text = '<strong>' + feature.get('name') + '</strong><br/>';
    text += '<p>Distance: <code>' + feature.get('distance') + '</code></p>';
    text += '<p>Estimated travel time: <code>' + feature.get('time') + '</code></p>';
    text = text.replace(/ /g, ' ');
  }
  return text;
};
// display the popup when user clicks on a route segment
map.on('click', function(evt) {
  var coordinate = evt.coordinate;
  var text = getFeatureInfo(coordinate);
  if (text) {
    popupOverlay.setPosition(coordinate);
    popup.innerHTML = text;
    popup.style.display = 'block';
  }
});

Precisamos registrar quando o usuário inicia ou para de arrastar um marcador para que possamos saber quando recalcular a rota. Faremos isso registrando o evento do clique do mouse.

// record start of click
map.on('pointerdown', function(evt) {
  pointerDown = true;
  popup.style.display = 'none';
});
// record end of click
map.on('pointerup', function(evt) {
  pointerDown = false;
  // if we were dragging a marker, recalculate the route
  if (currentMarker) {
    getVertex(currentMarker);
    getRoute();
    currentMarker = null;
 }
});

O último passo antes de trabalhar com a comunicação do cliente com o GeoServer é criar um temporizador que irá acionar a cada quarto de segundo, o que nos permite atualizar a rota periodicamente ao mover um marcador para uma nova localização.

// timer to update the route when dragging
window.setInterval(function(){
  if (currentMarker && changed) {
    getVertex(currentMarker);
    getRoute();
    changed = false;
  }
}, 250);

No código acima, podemos ver as chamadas para duas funções principais: getVertex e getRoute. Estes dois métodos realizam requisições WFS ao GeoServer para obter informações do recurso. getVertex recupera o vértice mais próximo na rede para a posição do marcador atual, enquanto getRoute calcula o caminho mais curto entre os dois marcadores.

O método getVertex utiliza as coordenadas atuais de um marcador e os passa como parâmetros x e y para o nearest_vertex (SQL View) que criamos no GeoServer. O requisição GetFeature do WFS será capturada como JSON e passada para a função loadVertex para o processamento.

// WFS to get the closest vertex to a point on the map
function getVertex(marker) {
  var coordinates = marker.getGeometry().getCoordinates();
  var url = geoserverUrl + '/wfs?service=WFS&version=1.0.0&' +
      'request=GetFeature&typeName=tutorial:nearest_vertex&' +
      'outputformat=application/json&' +
      'viewparams=x:' + coordinates[0] + ';y:' + coordinates[1];
  $.ajax({
     url: url,
     async: false,
     dataType: 'json',
     success: function(json) {
       loadVertex(json, marker == sourceMarker);
     }
  });
}

O loadVertex analisa a resposta do GeoServer e armazena o vértice mais próximo como o ponto de início ou o fim do nosso percurso. Vamos precisar do id do vértice mais tarde para solicitar a rota ao pgRouting.

// load the response to the nearest_vertex layer
function loadVertex(response, isSource) {
  var geojson = new ol.format.GeoJSON();
  var features = geojson.readFeatures(response);
  if (isSource) {
    if (features.length == 0) {
      map.removeLayer(routeLayer);
      source = null;
      return;
    }
    source = features[0];
  } else {
    if (features.length == 0) {
      map.removeLayer(routeLayer);
      target = null;
      return;
    }
    target = features[0];
  }
}

Tudo o que fizemos até agora foi construir a requisição final (WFS GetFeature) que vai realmente solicitar e exibir a rota. O shortest_path (SQL View) tem três parâmetros, o vértice de origem, o vértice destino e o custo (distância ou tempo).

function getRoute() {
  // set up the source and target vertex numbers to pass as parameters
  var viewParams = [
    'source:' + source.getId().split('.')[1],
    'target:' + target.getId().split('.')[1],
    'cost:time'
  ];
  var url = geoserverUrl + '/wfs?service=WFS&version=1.0.0&' +
      'request=GetFeature&typeName=tutorial:shortest_path&' +
      'outputformat=application/json&' +
      '&viewparams=' + viewParams.join(';');
  // create a new source for our layer
  routeSource = new ol.source.ServerVector({
    format: new ol.format.GeoJSON(),
    strategy: ol.loadingstrategy.all,
    loader: function(extent, resolution) {
      $.ajax({
        url: url,
        dataType: 'json',
        success: loadRoute,
        async: false
      });
    },
  });
  // remove the previous layer and create a new one
  map.removeLayer(routeLayer);
  routeLayer = new ol.layer.Vector({
    source: routeSource,
    style: new ol.style.Style({
      stroke: new ol.style.Stroke({
        color: 'rgba(0, 0, 255, 0.5)',
        width: 8
      })
    })
  });
  // add the new layer to the map
  map.addLayer(routeLayer);
}

A rota gerada será usado para criar uma nova camada e atualizar as informações da pop-up com os detalhes da rota, incluindo os locais de início e fim, a distância e o tempo de viagem.

// handle the response to shortest_path
var loadRoute = function(response) {
  selectSegment.getFeatures().clear();
  routeSource.clear();
  var features = routeSource.readFeatures(response)
  if (features.length == 0) {
    info.innerHTML = '';
    return;
  }
  routeSource.addFeatures(features);
  var time = 0;
  var dist = 0;
  features.forEach(function(feature) {
    time += feature.get('time');
    dist += feature.get('distance');
  });
  if (!pointerDown) {
    // set the route text
    var text = 'Travelling from ' + formatPlaces(source.get('name')) + ' to ' + formatPlaces(target.get('name')) + '. ';
    text += 'Total distance ' + formatDist(dist) + '. ';
    text += 'Estimated travel time: ' + formatTime(time) + '.';
    info.innerHTML = text;
    // snap the markers to the exact route source/target
    markerOverlay.getFeatures().clear();
    sourceMarker.setGeometry(source.getGeometry());
    targetMarker.setGeometry(target.getGeometry());
    markerOverlay.getFeatures().push(sourceMarker);
    markerOverlay.getFeatures().push(targetMarker);
  }
}

Nossa aplicação agora está completa! Você pode testá-lo, executando o SDK no modo de depuração:

suite-sdk debug routing

Veja como ficou o nosso mapa:

application

Este tutorial é uma tradução e adaptação livre do artigo “Building a Routing Application” publicado no site da Boundless.

Fonte: Boundless