Contents Prev: Visualizing data: restaurant density Next: Command line interface: osm4j-utils

Rendering a map

In this example we will render a map to a Swing panel. This is how the result will look like:

To build the map, we retrieve some data from the Overpass API using a bounding box query and read the data into memory. We will then select the features we would like to appear on the map and assemble their geometries. Whenever the panel's paintComponent() method is invoked, we render the objects onto the panel's canvas.

  public static void main(String[] args) throws IOException, OsmInputException
  {
    // This is the region we would like to render
    BBox bbox = new BBox(13.4554652.5122913.4664252.50761);
    int width = 800;
    int height = 600;
 
    // Define a query to retrieve some data
    String queryTemplate = "http://overpass-api.de/api/interpreter?data=(node(%f,%f,%f,%f);<;>;);out;";
    String query = String.format(queryTemplate, bbox.getLat2(),
        bbox.getLon1(), bbox.getLat1(), bbox.getLon2());
 
    // Open a stream
    InputStream input = new URL(query).openStream();
 
    // Create a reader and read all data into a data set
    OsmReader reader = new OsmXmlReader(input, false);
    InMemoryMapDataSet data = MapDataSetLoader.read(reader, truetrue,
        true);
 
    // The MercatorImage class knows how to transform input coordinates to
    // the selected region selected via bounding box
    MercatorImage mapImage = new MercatorImage(bbox, width, height);
 
    // Instantiate our class
    MapRendering panel = new MapRendering(bbox, mapImage, data);
    panel.setPreferredSize(new Dimension(width, height));
 
    // Setup a frame to show our panel
    JFrame frame = new JFrame("Map rendering");
    frame.setContentPane(panel);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.pack();
    frame.setVisible(true);
  }
 
  // Some fields that define the map colors and street line widths
  private Color cBackground = new Color(0xEEEEEE);
  private Color cBBox = Color.BLUE;
  private Color cStreetForeground = Color.WHITE;
  private Color cStreetBackground = new Color(0xDDDDDD);
  private Color cStreetText = Color.BLACK;
  private Color cBuildings = new Color(0xFFC2C2);
 
  private int widthStreetBackground = 14;
  private int widthStreetForeground = 10;
 
  // This is a set of values for the 'highway' key of ways that we will render
  // as streets
  private Set<String> validHighways = new HashSet<>(
      Arrays.asList(new String[] { "primary""secondary""tertiary",
          "residential""living_street" }));
 
  // This will be used to map geometry coordinates to screen coordinates
  private MercatorImage mercatorImage;
 
  // We need to keep the reference to the bounding box, so that we can create
  // a new MercatorImage if the size of our panel changes
  private BBox bbox;
 
  // The data set will be used as entity provider when building geometries
  private InMemoryMapDataSet data;
 
  // We build the geometries to be rendered during construction and store them
  // in these fields so that we don't have to recompute everything when
  // rendering.
  private List<Geometry> buildings = new ArrayList<>();
  private List<LineString> streets = new ArrayList<>();
  private Map<LineString, String> names = new HashMap<>();
 
  public MapRendering(BBox bbox, MercatorImage mercatorImage,
      InMemoryMapDataSet data)
  {
    this.bbox = bbox;
    this.mercatorImage = mercatorImage;
    this.data = data;
 
    // When the panel's size changes, define a new MercatorImage and trigger
    // a repaint on our panel
    addComponentListener(new ComponentAdapter() {
 
      @Override
      public void componentResized(ComponentEvent e)
      {
        refreshMercatorImage();
        repaint();
      }
 
    });
 
    buildRenderingData();
  }
 
  private void buildRenderingData()
  {
    // We create building geometries from relations and ways. Ways that are
    // part of multipolygon buildings may be tagged as buildings themselves,
    // however rendering them independently will fill the polygon holes they
    // are cutting out of the relations. Hence we store the ways found in
    // building relations to skip them later on when working on the ways.
    Set<OsmWay> buildingRelationWays = new HashSet<>();
    // We use this to find all way members of relations.
    EntityFinder wayFinder = EntityFinders.create(data,
        EntityNotFoundStrategy.IGNORE);
 
    // Collect buildings from relation areas...
    for (OsmRelation relation : data.getRelations().valueCollection()) {
      Map<String, String> tags = OsmModelUtil.getTagsAsMap(relation);
      if (tags.containsKey("building")) {
        MultiPolygon area = getPolygon(relation);
        if (area != null) {
          buildings.add(area);
        }
        try {
          wayFinder.findMemberWays(relation, buildingRelationWays);
        } catch (EntityNotFoundException e) {
          // cannot happen (IGNORE strategy)
        }
      }
    }
    // ... and also from way areas
    for (OsmWay way : data.getWays().valueCollection()) {
      if (buildingRelationWays.contains(way)) {
        continue;
      }
      Map<String, String> tags = OsmModelUtil.getTagsAsMap(way);
      if (tags.containsKey("building")) {
        MultiPolygon area = getPolygon(way);
        if (area != null) {
          buildings.add(area);
        }
      }
    }
 
    // Collect streets
    for (OsmWay way : data.getWays().valueCollection()) {
      Map<String, String> tags = OsmModelUtil.getTagsAsMap(way);
 
      String highway = tags.get("highway");
      if (highway == null) {
        continue;
      }
 
      Collection<LineString> paths = getLine(way);
 
      if (!validHighways.contains(highway)) {
        continue;
      }
 
      // Okay, this is a valid street
      for (LineString path : paths) {
        streets.add(path);
      }
 
      // If it has a name, store it for labeling
      String name = tags.get("name");
      if (name == null) {
        continue;
      }
      for (LineString path : paths) {
        names.put(path, name);
      }
    }
  }
 
  public void refreshMercatorImage()
  {
    mercatorImage = new MercatorImage(bbox, getWidth(), getHeight());
  }
 
  @Override
  protected void paintComponent(Graphics graphics)
  {
    super.paintComponent(graphics);
    Graphics2D g = (Graphics2D) graphics;
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);
 
    // Fill the background
    g.setColor(cBackground);
    g.fillRect(00, getWidth(), getHeight());
 
    // First render buildings
    g.setColor(cBuildings);
    for (Geometry building : buildings) {
      Shape polygon = Jts2Awt.toShape(building, mercatorImage);
      g.fill(polygon);
    }
 
    // First pass of street rendering: outlines
    g.setColor(cStreetBackground);
    g.setStroke(new BasicStroke(widthStreetBackground,
        BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
 
    for (LineString street : streets) {
      Path2D path = Jts2Awt.getPath(street, mercatorImage);
      g.draw(path);
    }
 
    // Second pass of street rendering: foreground
    g.setColor(cStreetForeground);
    g.setStroke(new BasicStroke(widthStreetForeground,
        BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
 
    for (LineString street : streets) {
      Path2D path = Jts2Awt.getPath(street, mercatorImage);
      g.draw(path);
    }
 
    // Now add labels where possible
    g.setFont(g.getFont().deriveFont(12f));
    g.setColor(cStreetText);
 
    for (LineString street : streets) {
      String name = names.get(street);
      if (name == null) {
        continue;
      }
      paintStreetLabel(g, street, name, mercatorImage);
    }
 
    // Also draw a rectangle around the query bounding box
    Geometry queryBox = new GeometryFactory().toGeometry(bbox.toEnvelope());
    Shape shape = Jts2Awt.toShape(queryBox, mercatorImage);
    g.setColor(cBBox);
    g.setStroke(new BasicStroke(2));
    g.draw(shape);
  }
 
  private void paintStreetLabel(Graphics2D g, LineString street, String name,
      CoordinateTransformer t)
  {
    // We will need this to measure the length of street names
    FontMetrics metrics = g.getFontMetrics();
 
    // For each segment
    for (int i = 1; i < street.getNumPoints(); i++) {
 
      // Segment is from c to d (WGS84 coordinates)
      Coordinate c = street.getCoordinateN(i - 1);
      Coordinate d = street.getCoordinateN(i);
 
      // Map coordinates to screen coordinates
      double cx = t.getX(c.x);
      double cy = t.getY(c.y);
      double dx = t.getX(d.x);
      double dy = t.getY(d.y);
 
      // Determine the length of the segment on the screen
      double len = Math.sqrt((dx - cx) * (dx - cx) + (dy - cy)
          * (dy - cy));
 
      // And also the length of the rendered street name
      int textLength = metrics.stringWidth(name);
 
      // Render only if there is enough space
      if (len <= textLength) {
        continue;
      }
 
      // We're going to modify the Graphics2D's transformation object to
      // render the text rotated and positioned, so we need to backup the
      // current transform object
      AffineTransform backup = g.getTransform();
 
      // We center the text on the segment so we calculate the offset
      // depending on the actual length of the text
      double offset = (len - textLength) / 2;
 
      // Define how to render text using transformations
      g.translate(cx, cy);
      g.rotate(Math.atan2(dy - cy, dx - cx));
      g.translate(offset, 4);
 
      // Draw!
      g.drawString(name, 00);
 
      // Undo our transformation
      g.setTransform(backup);
    }
  }
 
  private WayBuilder wayBuilder = new WayBuilder();
  private RegionBuilder regionBuilder = new RegionBuilder();
 
  private Collection<LineString> getLine(OsmWay way)
  {
    List<LineString> results = new ArrayList<>();
    try {
      WayBuilderResult lines = wayBuilder.build(way, data);
      results.addAll(lines.getLineStrings());
      if (lines.getLinearRing() != null) {
        results.add(lines.getLinearRing());
      }
    } catch (EntityNotFoundException e) {
      // ignore
    }
    return results;
  }
 
  private MultiPolygon getPolygon(OsmWay way)
  {
    try {
      RegionBuilderResult region = regionBuilder.build(way, data);
      return region.getMultiPolygon();
    } catch (EntityNotFoundException e) {
      return null;
    }
  }
 
  private MultiPolygon getPolygon(OsmRelation relation)
  {
    try {
      RegionBuilderResult region = regionBuilder.build(relation, data);
      return region.getMultiPolygon();
    } catch (EntityNotFoundException e) {
      return null;
    }
  }
Contents Prev: Visualizing data: restaurant density Next: Command line interface: osm4j-utils