Andreas Bruns

Softwareentwicklung für Oldenburg und Bremen

Location Picker auf Basis von OpenStreetMap

Ich stelle gerade ein Web-Projekt von Google Maps auf OpenStreetMap mit OpenLayers um. Das klappt größtenteils ganz gut, aber die Implementierung eines Location Pickers ging mir mit Google Maps leichter von der Hand als mit OpenStreetMap/OpenLayers. Das lag vielleicht auch daran, dass es für Google Maps weit mehr Beispiele gibt und ich ein passendes OpenStreetMap/OpenLayers-Beispiel nicht sofort gefunden habe. Dieses Beispiel hilft hoffentlich anderen Entwicklern, die ebenfalls einen Location Picker mit OpenStreetMap und OpenLayers realisieren wollen.

Mit meiner kleinen Beispielanwendung soll der Anwender Name, Adresse und Geo-Koordinaten von Sehenswürdigkeiten erfassen können. Die Anwendung besteht aus einer HTML-Seite mit JavaScript und einer Server-Komponente auf Basis von Java-Servlets. Anhand eines Markers auf einer Karte kann der Anwender die Position der Sehenswürdigkeit markieren, indem er den Marker auf der Karte verschiebt. Die Adresse kann händisch eingegeben werden oder sie wird von einem Geocoder-Dienst anhand der Geo-Koordinaten automatisch ermittelt. Ach, was erzähl ich – ein Bild sagt ja schon alles:

Location Picker mit OpenStreetMap


Zunächst einmal benötigen wir eine HTML-Seite mit den Formularfeldern und der OpenStreetMap-Karte. Die Formulardaten für eine Sehenswürdigkeit werden nach dem Abschicken des Formulars von einem Java-Servlet ausgewertet und innerhalb des Servlets könnte die angegebene Sehenswürdigkeit in einer Datenbank abgespeichert werden. Bei der folgenden HTML-Datei könnte der Anwender die Karte und die Koordinaten des Mauszeigers bereits verwenden, um die Formularfelder der Sehenswürdigkeit händisch zu befüllen. In Verbindung mit dem angegebenen PoiServlet (POI = Point of Interest) haben wir bereits eine einfache funktionierende Webanwendung. Ich versende die Formular-Daten natürlich per POST-Methode, sodass wir im Servlet auch die doPost()-Methode implementieren müssen.

<html>
  <head>
  <title>Example OSM LocationPicker</title>
  <script type="text/javascript" src="http://www.openlayers.org/api/OpenLayers.js"></script>

  <script>
  function init(){
    var proj4326 = new OpenLayers.Projection("EPSG:4326");
    var projmerc = new OpenLayers.Projection("EPSG:900913");

    var mapCenterPositionAsLonLat = new OpenLayers.LonLat(8.80755, 53.07621);
    var mapCenterPositionAsMercator = mapCenterPositionAsLonLat.transform(proj4326, projmerc);
    var mapZoom = 15;

    map = new OpenLayers.Map("locationPickerMap", {
       controls: [
          new OpenLayers.Control.KeyboardDefaults(),
          new OpenLayers.Control.Navigation(),
          new OpenLayers.Control.LayerSwitcher({'ascending':false}),
          new OpenLayers.Control.PanZoomBar(),
          new OpenLayers.Control.MousePosition()
        ],
        maxExtent:
          new OpenLayers.Bounds(-20037508.34,-20037508.34,
                       20037508.34, 20037508.34),
        numZoomLevels: 18,
        maxResolution: 156543,
        units: 'm',
        projection: projmerc,
        displayProjection: proj4326
    } );

    var osmLayer = new OpenLayers.Layer.OSM("OpenStreetMap");
    map.addLayer(osmLayer);
    map.setCenter(mapCenterPositionAsMercator, mapZoom);
  }
  </script>
  </head>

  <body onload="init();">
     <h1>Sehenswürdigkeit anlegen</h1>
     <div id="locationPickerMap" style="width:500px; height:400px;"></div>
     <form action="poi" method="post">
       <table>
           <tr>
             <td><b>Name: </b></td>
             <td><input id="poiName" name="poiName" type="text" size="50" value="Die Bremer Stadtmusikanten"/></td>
           </tr>
           <tr>
             <td><b>Adresse: </b></td>
             <td> <input id="poiAddress" name="poiAddress" type="text" size="50" value="Schoppensteel 1, 28195 Bremen, Germany"/></td>
           </tr>
           <tr>
             <td><b>Longitude: </b></td>
             <td><input id="poiLongitude" name="poiLongitude" type="text" size="50" value="8.80755"/></td>
           </tr>
           <tr>
             <td><b>Latitude: </b></td>
             <td><input id="poiLatitude" name="poiLatitude" type="text" size="50" value="53.07621"/></td>
           </tr>
           <tr>
             <td><input type="submit" value="Abschicken" /></td>
           </tr>
       </table>
    </form>
  </body>
</html>

 

public class PoiServlet extends HttpServlet {
   @Override
   protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
      String name = request.getParameter("poiName");
      String address = request.getParameter("poiAddress");
      String lat = request.getParameter("poiLatitude");
      String lon = request.getParameter("poiLongitude");
      String poiAddress = name + " (" + lon + "," + lat + ") => " + address;

      System.out.println("Sehenswürdigkeit erstellt: " + poiAddress);

      response.setContentType("text/html");
      response.setCharacterEncoding("UTF-8");
      response.getWriter().write("<html><body>Danke - viel Spaß bei:<br> " + poiAddress + "</body></html>");
   }
}

Jetzt kommen wir zu dem spannenden Teil, denn wir erweitern die Karte um einen LocationPickerLayer, auf dem sich der Marker des Location Pickers befindet. Dem LocationPickerLayer fügen wir ein Objekt der Klasse OpenLayers.Feature.Vector hinzu, dessen Style wir mit dem Bild eines Markers konfigurieren. Der Marker muss mit einem Offset versehen werden, sodass die Spitze des Pfeils auf die tatsächliche Position zeigt. Damit wir beim Verschieben des Markers benachrichtigt werden, müssen wir der Karte ein Control vom Typ OpenLayers.Control.DragFeature hinzufügen. In der onComplete()-Methode des DragFeatures werten wir die aktuelle Position des verschobenen Markers aus und befüllen damit die Formularfelder der Geo-Koordinaten.

        var locationPickerLayer = new OpenLayers.Layer.Vector("LocationPicker Marker");
        map.addLayer(locationPickerLayer);
        var locationPickerPoint = new OpenLayers.Geometry.Point(mapCenterPositionAsMercator.lon, mapCenterPositionAsMercator.lat);
        var locationPickerMarkerStyle = {externalGraphic: 'poi.png', graphicHeight: 37, graphicWidth: 32, graphicYOffset: -37, graphicXOffset: -16 };
        var locationPickerVector = new OpenLayers.Feature.Vector(locationPickerPoint, null, locationPickerMarkerStyle);
        locationPickerLayer.addFeatures(locationPickerVector);

        var dragFeature = new OpenLayers.Control.DragFeature(locationPickerLayer,
        {
          onComplete:  function( feature, pixel ) {
            var selectedPositionAsMercator = new OpenLayers.LonLat(locationPickerPoint.x, locationPickerPoint.y);
            var selectedPositionAsLonLat = selectedPositionAsMercator.transform(projmerc, proj4326);

            document.getElementById("poiLatitude").value = selectedPositionAsLonLat.lat;
            document.getElementById("poiLongitude").value = selectedPositionAsLonLat.lon;
          }
        });
        map.addControl(dragFeature);
        dragFeature.activate();

Als nettes Gimmick lassen wir uns jetzt noch vom Server zu einer Geo-Koordinate eine passende Adresse liefern. Dazu erweitern wir die onComplete()-Methode um einen AJAX-Aufruf. Per GET-Request mit den Geo-Koordinaten als Parameter befragen wir den Server nach der Adresse. Die asynchrone Antwort des Servers geben wir an einen JavaScript-Handler weiter, der den Antwort-Text in das Formularfeld der Adresse einträgt.

  onComplete: function( feature, pixel ) {
    ...
    var request = OpenLayers.Request.GET({
      url: "address",
      params: {lon: selectedPositionAsLonLat.lon, lat: selectedPositionAsLonLat.lat },
      callback: handler
    });
  }

  function handler(request) {
    document.getElementById("poiAddress").value = request.responseText;
  }

Serverseitig werten wir im AddressServlet die empfangenen Geo-Koordinaten aus und stellen eine Anfrage an den Google-Geocoder, zum Beispiel: http://maps.google.com/maps/api/geocode/json?address=53.0771,8.80596&sensor=false

Die JSON-Antwort ist recht ausführlich. Für uns bietet das Element „formatted_address“ eine gute Adresse, wobei wir auf das passende Encoding (UTF-8) für korrekte Umlaute achten sollten. Der Programmcode solch eines AddressServlets kann folgendermaßen aussehen. In der web.xml müssen unsere beiden Servlets natürlich ebenfalls eingetragen sein.

public class AddressServlet extends HttpServlet {

   @Override
   protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
      String lon = request.getParameter("lon");
      String lat = request.getParameter("lat");

      String urlAsString = "http://maps.google.com/maps/api/geocode/json?address=" + lat + "," + lon + "&sensor=false";
      String contentAsString = contentOfUrl(urlAsString);
      String address = addressOfContent(contentAsString);

      System.out.println(lon+ "," + lat + " => " + address);

      response.setContentType("text/plain");
      response.setCharacterEncoding("UTF-8");
      response.getWriter().write(address);
   }

   private String addressOfContent(String content) {
      String startIndexString = "formatted_address" : "";
      int startIndex = content.indexOf(startIndexString) + startIndexString.length();
      int endIndex = content.indexOf(""geometry");
      return content.substring(startIndex, endIndex).trim().replace("",", "");
   }

   private String contentOfUrl(String urlString) throws IOException {
      StringBuffer contentBuffer = new StringBuffer();
      BufferedReader in = new BufferedReader(new InputStreamReader(new URL(urlString).openStream(), "UTF-8"));
      String inputLine;
      while ((inputLine = in.readLine()) != null) {
         contentBuffer.append(inputLine);
      }
      in.close();
      return contentBuffer.toString();
   }
}

 

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
	<servlet>
		<servlet-name>AddressServlet</servlet-name>
		<servlet-class>de.bruns.example.osm.locationpicker.AddressServlet</servlet-class>
	</servlet>
	<servlet>
		<servlet-name>PoiServlet</servlet-name>
		<servlet-class>de.bruns.example.osm.locationpicker.PoiServlet</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>AddressServlet</servlet-name>
		<url-pattern>/address</url-pattern>
	</servlet-mapping>
	<servlet-mapping>
		<servlet-name>PoiServlet</servlet-name>
		<url-pattern>/poi</url-pattern>
	</servlet-mapping>
</web-app>

Der Programmcode des Beispiels ist unter Github verfügbar und mit einer laufenden Beispielanwendung kann man hier herumspielen – viel Spaß…