Andreas Bruns

Softwareentwicklung für Oldenburg und Bremen

Openstreetmap-Karten per Vektorgrafiken mit Mapsforge anzeigen

In mehreren Beiträgen habe ich bereits dargestellt, dass Openstreetmap (OSM) eine super Alternative zu Google Maps ist, um interaktive Karten in die eigene Webseite oder in die eigene Software einzubinden. Sobald die Anforderungen bekannt sind, lohnt sich ein Blick in diese Übersicht von OSM, um aus der Vielzahl von verfügbaren Bibliotheken die passende zu finden.

Mit den folgenden Anforderungen können wir beispielsweise die Anzahl schon gut eingrenzen:

  • für welche Plattform? Web, Android, iOS, Linux, macOS, Cross-Plattform
  • muss die Anwendung ohne Internet (Offline) funktionieren können?
  • muss es sich um Open Source handeln?
  • mit welcher Programmiersprache ist die Bibliothek umgesetzt?
  • Performanz der Anwendung?
  • Einsatz eigener Karten-Server nötig oder reichen externe OSM-Server?
  • sollen Vektorgrafiken oder Rastergrafiken eingesetzt werden?
  • reicht 2D-Darstellung oder muss es sogar 3D-Darstellung sein

Online / Offline -> Raster- oder Vektorgrafik

In meinen bisherigen Artikeln habe ich Anwendungen auf Basis von Rastergrafiken vorgestellt, die vorgefertigte Kartengrafiken bei Bedarf von einem Tile-Server im Internet herunterladen und dann passend darstellen.

Bei einer Android-Anwendung (z.B. meine Nette Toilette App) geschieht das durch die mitgelieferte Google Maps Bibliothek mit der Programmiersprache Java. Innerhalb einer Webanwendung erledigen die JavaScript-Bibliotheken OpenLayers oder Leaflet die entsprechende Darstellung.

Falls die benötigte Anwendung allerdings keinen permanenten Internetzugriff hat, müssten alle Rastergrafiken auf dem Gerät bereits vorliegen und würden enorm viel Speicherplatz beanspruchen. In solch einem Anwendungsfall sollten lediglich die platzsparenderen OSM-Daten auf dem Gerät gespeichert sein. Die Anwendung errechnet dann bei Bedarf die Grafik zu dem Kartenausschnitt, wobei zumeist Vektorgrafiken zum Einsatz kommen.

Mapsforge: Offline + Vektorgrafik

Mapsforge ist eine sehr ausgereifte Java-Bibliothek, um interaktive Karten offline anzeigen zu können:

  • offline-fähige Darstellung von interaktiven Karten
  • Plattform: Android, Desktop-App (basiert auf Java)
  • Open Source
  • OSM-Daten liegen in eigenem Map-Format vor
  • Map-Daten lassen sich für verschiedene Regionen herunterladen: https://download.mapsforge.org/
  • Mapsforge berechnet aus den Map-Daten die Vektorgrafiken
  • Caching von bereits erzeugten Grafiken

Schnellstart

Mapsforge ist vorrangig für den Einsatz in Android-Apps konzipiert, sodass sich die meisten Beispiele auf Android beziehen. Für den Desktop-Einsatz ist es ebenfalls gut geeignet und es wird auch ein Beispiel mitgeliefert. Wenn Java und Git installiert sind, sollte man mit den folgenden Befehlen das Mapsforge-Beispiel starten können und eine Karte von Bremen und Bremerhaven angezeigt bekommen:

git clone https://github.com/mapsforge/mapsforge.git
cd mapsforge
wget https://download.mapsforge.org/maps/v4/europe/germany/bremen.map
./gradlew --project-dir=mapsforge-samples-awt build fatJar
java -jar mapsforge-samples-awt/build/libs/mapsforge-samples-awt-master-SNAPSHOT-jar-with-dependencies.jar bremen.map 
Mapsforge: Desktop-Beispiel
Mapsforge: Desktop-Beispiel

Eigene Desktop-App mit Marker-Interaktion

Bei dem folgenden Beispiel wird eine eigene Java-Applikation mit einer Mapsforge-Karte erstellt, die die Position der Bremer-Stadtmusikanten auf einer Karte darstellt.

Zunächst erstellen wir mit Gradle im Terminal die entsprechende Projektstruktur.

mkdir example-mapsforge; cd "$_"
gradle init \
  --type java-application \
  --dsl groovy \
  --project-name example-mapsforge \
  --package de.bruns.example.mapsforge \
  --test-framework spock

Anschließend fügen wir unsere Abhängigkeiten in die ‚build.gradle‘-Datei hinzu. Dabei müssen wir für die benötigte Bibliothek „svgSalamander“ ein spezielles Repository angeben:

plugins {
    id 'java'
}

repositories {
    jcenter()
    maven {
        url "https://repository.mulesoft.org/nexus/content/repositories/public"
    }
}

dependencies {
    implementation group: 'org.mapsforge', name: 'mapsforge-core', version: '0.13.0'
    implementation group: 'org.mapsforge', name: 'mapsforge-map-awt', version: '0.13.0'
    implementation group: 'org.mapsforge', name: 'mapsforge-themes', version: '0.13.0'
    implementation group: 'com.github.blackears', name: 'svgSalamander', version: 'v1.1.1'
}

// create a fat-jar
jar {
    manifest {
        attributes(
          'Main-Class': 'de.bruns.example.mapsforge.App'
        )
    }
    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

Anschließend sind folgende Schritte nötig, um eine lauffähige Anwendung zu erhalten:

  • eine Marker-Bild ablegen unter: src/main/resources/poi.png
  • die Bremen-Daten als bremen.map im Hauptverzeichnis ablegen , z.B.:
    wget https://download.mapsforge.org/maps/v4/europe/germany/bremen.map
  • die Java-Datei src/main/java/de/bruns/example/mapsforge/App.java durch den nachfolgenden Code ersetzen
  • im Terminal mit Gradle das FatJar bauen: ./gradlew jar
  • Applikation im Terminal starten: java -jar build/libs/example-mapsforge.jar
package de.bruns.example.mapsforge;

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.WindowConstants;
import org.mapsforge.core.graphics.Bitmap;
import org.mapsforge.core.graphics.GraphicFactory;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.core.model.MapPosition;
import org.mapsforge.core.model.Point;
import org.mapsforge.map.awt.graphics.AwtBitmap;
import org.mapsforge.map.awt.graphics.AwtGraphicFactory;
import org.mapsforge.map.awt.util.AwtUtil;
import org.mapsforge.map.awt.view.MapView;
import org.mapsforge.map.datastore.MultiMapDataStore;
import org.mapsforge.map.layer.cache.TileCache;
import org.mapsforge.map.layer.overlay.Marker;
import org.mapsforge.map.layer.renderer.TileRendererLayer;
import org.mapsforge.map.reader.MapFile;
import org.mapsforge.map.rendertheme.InternalRenderTheme;

public final class App {

  private static final String PIN_FILE = "/poi.png";
  private static final String MAP_FILE = "bremen.map";
  private static final LatLong LAT_LONG = new LatLong(53.3, 8.7);
  private static final LatLong LAT_LONG_PIN = new LatLong(53.076199, 8.80755);
  private static final byte ZOOM_LEVEL = 8;

  private static final int TILE_SIZE = 512;
  private static final GraphicFactory GRAPHIC_FACTORY = AwtGraphicFactory.INSTANCE;
  private static final InternalRenderTheme RENDER_THEME = InternalRenderTheme.OSMARENDER;

  public static void main(String[] args) {
    final MapView mapView = createMap();
    final JMenuBar menu = createMenu(mapView);

    final JFrame frame = new JFrame();
    frame.add(mapView);
    frame.setJMenuBar(menu);
    frame.setTitle("Mapsforge Samples");
    frame.pack();
    frame.setSize(1024, 768);
    frame.setLocationRelativeTo(null);
    frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    frame.addWindowListener(new WindowAdapter() {
      @Override
      public void windowClosing(WindowEvent e) {
        mapView.destroyAll();
        AwtGraphicFactory.clearResourceMemoryCache();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
      }
    });
    frame.setVisible(true);
  }

  static class PinMarker extends Marker {

    private final String description;

    public PinMarker(LatLong latLong, Bitmap bitmap, int horizontalOffset, int verticalOffset, String description) {
      super(latLong, bitmap, horizontalOffset, verticalOffset);
      this.description = description;
    }

    @Override
    public boolean onTap(LatLong tapLatLong, Point layerXY, Point tapXY) {
      double centerX = layerXY.x + getHorizontalOffset();
      double centerY = layerXY.y + getVerticalOffset();
      double radiusX = (getBitmap().getWidth() / 2) * 1.1;
      double radiusY = (getBitmap().getHeight() / 2) * 1.1;
      double distX = Math.abs(centerX - tapXY.x);
      double distY = Math.abs(centerY - tapXY.y);
      if (distX < radiusX && distY < radiusY) {
        JOptionPane.showMessageDialog(null, "Pin: " + description);
        return true;
      }
      return false;
    }
  }

  private static MapView createMap() {
    // AWT-Container MapView erstellen
    final MapView mapView = new MapView();
    mapView.getModel().mapViewPosition.setZoomLevelMin((byte) 6);
    mapView.getModel().mapViewPosition.setZoomLevelMax((byte) 20);
    mapView.getModel().displayModel.setFixedTileSize(TILE_SIZE);
    mapView.getModel().mapViewPosition.setMapPosition(new MapPosition(LAT_LONG, ZOOM_LEVEL));

    // MapDataStore mit der Bremen-Karte
    MultiMapDataStore mapDataStore = new MultiMapDataStore(MultiMapDataStore.DataPolicy.RETURN_ALL);
    mapDataStore.addMapDataStore(new MapFile(MAP_FILE), false, false);

    // Cache für die erstellten Kartenausschnitte
    TileCache tileCache = AwtUtil.createTileCache(TILE_SIZE,
      mapView.getModel().frameBufferModel.getOverdrawFactor(), 1024,
      new File(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString()));

    // Layer zur Anzeige der Karte
    TileRendererLayer tileRendererLayer = new TileRendererLayer(
      tileCache, mapDataStore, mapView.getModel().mapViewPosition, false, true, false, GRAPHIC_FACTORY, null) {
      @Override
      public boolean onTap(LatLong tapLatLong, Point layerXY, Point tapXY) {
        JOptionPane.showMessageDialog(null, "Position: " + tapLatLong);
        return true;
      }
    };
    tileRendererLayer.setXmlRenderTheme(RENDER_THEME);
    mapView.getLayerManager().getLayers().add(tileRendererLayer);

    return mapView;
  }

  private static JMenuBar createMenu(MapView mapView) {
    JMenuBar menubar = new JMenuBar();
    JMenu menuNavigation = new JMenu("Navigation");
    menubar.add(menuNavigation);

    JMenuItem zoomIn = new JMenuItem("Zoom in");
    zoomIn.addActionListener(e -> mapView.getModel().mapViewPosition.setZoomLevel((byte) (mapView.getModel().mapViewPosition.getZoomLevel() + 1)));
    menuNavigation.add(zoomIn);

    JMenuItem zoomOut = new JMenuItem("Zoom out");
    zoomOut.addActionListener(e -> mapView.getModel().mapViewPosition.setZoomLevel((byte) (mapView.getModel().mapViewPosition.getZoomLevel() - 1)));
    menuNavigation.add(zoomOut);

    JMenu menuMarker = new JMenu("Marker");
    menubar.add(menuMarker);

    JMenuItem addDescription = new JMenuItem("Add pin (with message)");
    addDescription.addActionListener(e -> {
      try {
        InputStream resourceAsStream = App.class.getResourceAsStream(PIN_FILE);
        AwtBitmap icon = new AwtBitmap(resourceAsStream);
        icon.scaleTo(31, 43);
        Marker pinMarker = new PinMarker(LAT_LONG_PIN, icon, 0, -icon.getHeight() / 2, "Bremer Stadtmusikanten");
        mapView.getLayerManager().getLayers().add(pinMarker);
      } catch (IOException ex) {
        ex.printStackTrace();
      }
    });
    menuMarker.add(addDescription);
    return menubar;
  }
}
Anklickbarer Marker der „Bremer Stadmusikanten“

Mapsforge-Beispiele für Android

Mapsforge enthält einige Demo-Beispiele, die größtenteils auf Android basieren, aber natürlich auch für die Desktop-Variante nützliche Einblicke liefern. Für das Ausführen der Beispiele gehe ich folgendermaßen vor:

  1. Mapsforge bei GitHub auschecken:
    git clone https://github.com/mapsforge/mapsforge.git
  2. Android-Studio installieren, falls noch nicht installiert: Download
  3. Mapsforge in Android Studio importieren: File -> Open -> Mapsforge-Verzeichnis
  4. Android Studio sollte jetzt das gesamte Mapsforge-Verzeichnis bauen und das Android-Verzeichnis „mapsforge-samples-android“ im Projekt anzeigen
  5. Android Device erstellen, falls noch nicht vorhanden
  6. „mapsforge-samples-android“-Projekt per Run ausführen, sodass die App auf dem Android-App gestartet und angezeigt wird
  7. als nächstes benötigen wir noch Map-Kartendaten von Berlin:
    wget https://download.mapsforge.org/maps/v4/europe/germany/berlin.map
  8. die Map-Datei speichern wir per „Device File Manager“ auf der SD-Karte des Android-Devices unter: /sdcard/Android/data/org.mapsforge.samples.android/files/berlin.map
  9. jetzt können wir die Mapsforge-Beispiele ausprobieren und natürlich direkt im Android-Studio anpassen
Mapsforge: Android-Beispiel

Fazit

Damit haben wir Mapsforge als Offline-Anwendung auf dem Desktop und auf Android erfolgreich gestartet. Jetzt wird es Zeit, eine eigene Anwendung basierend auf Mapsforge zu entwickeln (z.B. mit Mapsforge-POI) . Mein Beispiel Projekt ist übrigens auf Github (https://github.com/me4bruno/blog-examples -> example-mapsforge) verfügbar. Viel Spass 🙂

Kommentare sind geschlossen.