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.
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.
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:
Este tutorial é uma tradução e adaptação livre do artigo “Building a Routing Application” publicado no site da Boundless.
Fonte: Boundless