All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentripplanner.ext.debugrastertiles.EdgeVertexTileRenderer Maven / Gradle / Ivy

The newest version!
package org.opentripplanner.ext.debugrastertiles;

import com.jhlabs.awt.ShapeStroke;
import com.jhlabs.awt.TextStroke;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.image.BufferedImage;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import org.locationtech.jts.awt.IdentityPointTransformation;
import org.locationtech.jts.awt.PointShapeFactory;
import org.locationtech.jts.awt.ShapeWriter;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.linearref.LengthLocationMap;
import org.locationtech.jts.linearref.LocationIndexedLine;
import org.locationtech.jts.operation.buffer.BufferParameters;
import org.locationtech.jts.operation.buffer.OffsetCurveBuilder;
import org.opentripplanner.framework.geometry.GeometryUtils;
import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.model.edge.StreetEdge;
import org.opentripplanner.street.model.vertex.StreetVertex;
import org.opentripplanner.street.model.vertex.Vertex;

/**
 * A TileRenderer implementation which get all edges/vertex in the bounding box of the tile, and
 * call a EdgeVertexRenderer for getting rendering attributes of each (color, string label...).
 *
 * @author laurent
 */
public class EdgeVertexTileRenderer implements TileRenderer {

  private final EdgeVertexRenderer evRenderer;

  public EdgeVertexTileRenderer(EdgeVertexRenderer evRenderer) {
    this.evRenderer = evRenderer;
  }

  @Override
  public int getColorModel() {
    return BufferedImage.TYPE_INT_ARGB;
  }

  @Override
  public void renderTile(TileRenderContext context) {
    float lineWidth = (float) (1.0f + 3.0f / Math.sqrt(context.metersPerPixel));

    // Grow a bit the envelope to prevent rendering glitches between tiles
    Envelope bboxWithMargins = context.expandPixels(lineWidth * 2.0, lineWidth * 2.0);

    var streetIndex = context.graph.getStreetIndex();

    Collection vertices = streetIndex
      .getVerticesForEnvelope(bboxWithMargins)
      .stream()
      .sorted(evRenderer::vertexSorter)
      .toList();

    Collection edges = streetIndex
      .getEdgesForEnvelope(bboxWithMargins)
      .stream()
      .distinct()
      .sorted(evRenderer::edgeSorter)
      .toList();

    // Note: we do not use the transform inside the shapeWriter, but do it ourselves
    // since it's easier for the offset to work in pixel size.
    ShapeWriter shapeWriter = new ShapeWriter(
      new IdentityPointTransformation(),
      new PointShapeFactory.Point()
    );

    Stroke stroke = new BasicStroke(
      lineWidth * 1.4f,
      BasicStroke.CAP_ROUND,
      BasicStroke.JOIN_BEVEL
    );
    Stroke halfStroke = new BasicStroke(
      lineWidth * 0.6f + 1.0f,
      BasicStroke.CAP_ROUND,
      BasicStroke.JOIN_BEVEL
    );
    Stroke halfDashedStroke = new BasicStroke(
      lineWidth * 0.6f + 1.0f,
      BasicStroke.CAP_BUTT,
      BasicStroke.JOIN_BEVEL,
      1.0f,
      new float[] { 4 * lineWidth, lineWidth },
      2 * lineWidth
    );
    Stroke arrowStroke = new ShapeStroke(
      new Polygon(new int[] { 0, 0, 30 }, new int[] { 0, 20, 10 }, 3),
      lineWidth / 2,
      5.0f * lineWidth,
      2.5f * lineWidth
    );
    BasicStroke thinStroke = new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);

    Font font = new Font(Font.SANS_SERIF, Font.PLAIN, Math.round(lineWidth));
    Font largeFont = new Font(Font.SANS_SERIF, Font.PLAIN, Math.round(lineWidth * 1.5f));
    FontMetrics largeFontMetrics = context.graphics.getFontMetrics(largeFont);
    context.graphics.setFont(largeFont);
    context.graphics.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON
    );
    context.graphics.setRenderingHint(
      RenderingHints.KEY_TEXT_ANTIALIASING,
      RenderingHints.VALUE_TEXT_ANTIALIAS_ON
    );

    BufferParameters bufParams = new BufferParameters();
    bufParams.setSingleSided(true);
    bufParams.setJoinStyle(BufferParameters.JOIN_BEVEL);

    // Render all edges
    for (Edge edge : edges) {
      Geometry edgeGeom = edge.getGeometry();
      boolean hasGeom = true;
      if (edgeGeom == null) {
        Coordinate[] coordinates = new Coordinate[] {
          edge.getFromVertex().getCoordinate(),
          edge.getToVertex().getCoordinate(),
        };
        edgeGeom = GeometryUtils.getGeometryFactory().createLineString(coordinates);
        hasGeom = false;
      }

      var evAttrsOpt = evRenderer.renderEdge(edge);

      if (evAttrsOpt.isEmpty()) {
        continue;
      }
      EdgeVisualAttributes evAttrs = evAttrsOpt.get();

      Geometry midLineGeom = context.transform.transform(edgeGeom);
      OffsetCurveBuilder offsetBuilder = new OffsetCurveBuilder(new PrecisionModel(), bufParams);
      Coordinate[] coords = offsetBuilder.getOffsetCurve(
        midLineGeom.getCoordinates(),
        lineWidth * 0.4
      );
      if (coords.length < 2) continue; // Can happen for very small edges (<1mm)
      LineString offsetLine = GeometryUtils.makeLineString(coords);
      Shape midLineShape = shapeWriter.toShape(midLineGeom);
      Shape offsetShape = shapeWriter.toShape(offsetLine);

      context.graphics.setStroke(hasGeom ? halfStroke : halfDashedStroke);

      if (evRenderer.hasEdgeSegments(edge)) {
        LocationIndexedLine line = new LocationIndexedLine(offsetLine);
        LengthLocationMap locater = new LengthLocationMap(offsetLine);
        var offsetLength = offsetLine.getLength();

        var previousLocation = line.getStartIndex();
        for (var it : evRenderer.edgeSegments(edge)) {
          var currentLocation = locater.getLocation(offsetLength * it.position());
          var segmentGeometry = line.extractLine(previousLocation, currentLocation);
          var segmentShape = shapeWriter.toShape(segmentGeometry);
          context.graphics.setColor(it.color());
          context.graphics.draw(segmentShape);

          previousLocation = currentLocation;
        }
      } else {
        context.graphics.setColor(evAttrs.color);
        context.graphics.draw(offsetShape);
      }
      if (lineWidth > 6.0f) {
        context.graphics.setColor(Color.WHITE);
        context.graphics.setStroke(arrowStroke);
        context.graphics.draw(offsetShape);
      }
      if (lineWidth > 4.0f) {
        context.graphics.setColor(Color.BLACK);
        context.graphics.setStroke(thinStroke);
        context.graphics.draw(midLineShape);
      }
      if (evAttrs.label != null && lineWidth > 8.0f) {
        context.graphics.setColor(Color.BLACK);
        context.graphics.setStroke(
          new TextStroke(
            "    " + evAttrs.label + "                              ",
            font,
            false,
            true
          )
        );
        context.graphics.draw(offsetShape);
      }
    }

    // Render all vertices
    for (Vertex vertex : vertices) {
      Point point = GeometryUtils.getGeometryFactory().createPoint(vertex.getCoordinate());
      var vvAttrsOp = evRenderer.renderVertex(vertex);

      if (vvAttrsOp.isEmpty()) {
        continue;
      }
      var vvAttrs = vvAttrsOp.get();

      Point tilePoint = (Point) context.transform.transform(point);
      Shape shape = shapeWriter.toShape(tilePoint);

      context.graphics.setColor(vvAttrs.color);
      context.graphics.setStroke(stroke);
      context.graphics.draw(shape);
      if (
        vvAttrs.label != null && lineWidth > 6.0f && context.bbox.contains(point.getCoordinate())
      ) {
        context.graphics.setColor(Color.BLACK);
        int labelWidth = largeFontMetrics.stringWidth(vvAttrs.label);
        /*
         * Poor man's solution: stay on the tile if possible. Otherwise the renderer would
         * need to expand the envelope by an unbounded amount (max label size).
         */
        double x = tilePoint.getX();
        if (x + labelWidth > context.tileWidth) x -= labelWidth;
        context.graphics.drawString(vvAttrs.label, (float) x, (float) tilePoint.getY());
      }
    }
  }

  @Override
  public String getName() {
    return evRenderer.getName();
  }

  public interface EdgeVertexRenderer {
    Comparator defaultVertexComparator = Comparator.comparing((Vertex v) ->
      v instanceof StreetVertex
    ).reversed();
    Comparator defaultEdgeComparator = Comparator.comparing(
      (Edge e) -> e.getGeometry() != null
    ).thenComparing(e -> e instanceof StreetEdge);

    /**
     * @param e The edge being rendered.
     * @return  edge to render, or empty otherwise.
     */
    Optional renderEdge(Edge e);

    /**
     * @param v The vertex being rendered.
     * @return  vertex to render, or empty otherwise.
     */
    Optional renderVertex(Vertex v);

    /**
     * Name of this tile Render which would be shown in frontend
     *
     * @return Name of tile render
     */
    String getName();

    default boolean hasEdgeSegments(Edge edge) {
      return false;
    }

    default Iterable edgeSegments(Edge edge) {
      return List.of();
    }

    default int vertexSorter(Vertex v1, Vertex v2) {
      return defaultVertexComparator.compare(v1, v2);
    }

    default int edgeSorter(Edge e1, Edge e2) {
      return defaultEdgeComparator.compare(e1, e2);
    }
  }

  public record EdgeVisualAttributes(Color color, String label) {
    public static Optional optional(Color color, String label) {
      return Optional.of(new EdgeVisualAttributes(color, label));
    }
  }

  public record VertexVisualAttributes(Color color, String label) {
    public static Optional optional(Color color, String label) {
      return Optional.of(new VertexVisualAttributes(color, label));
    }
  }

  record EdgeSegmentColor(Double position, Color color) {}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy