
org.opentripplanner.inspector.EdgeVertexTileRenderer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otp Show documentation
Show all versions of otp Show documentation
The OpenTripPlanner multimodal journey planning system
package org.opentripplanner.inspector;
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 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.common.model.T2;
import org.opentripplanner.routing.edgetype.StreetEdge;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.vertextype.StreetVertex;
import org.opentripplanner.util.geometry.GeometryUtils;
/**
* 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
EdgeVisualAttributes evAttrs = new EdgeVisualAttributes();
for (Edge edge : edges) {
evAttrs.color = null;
evAttrs.label = null;
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;
}
boolean render = evRenderer.renderEdge(edge, evAttrs);
if (!render) continue;
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 p2 : evRenderer.edgeSegments(edge)) {
var currentLocation = locater.getLocation(offsetLength * p2.first);
var segmentGeometry = line.extractLine(previousLocation, currentLocation);
var segmentShape = shapeWriter.toShape(segmentGeometry);
context.graphics.setColor(p2.second);
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
VertexVisualAttributes vvAttrs = new VertexVisualAttributes();
for (Vertex vertex : vertices) {
vvAttrs.color = null;
vvAttrs.label = null;
Point point = GeometryUtils.getGeometryFactory().createPoint(vertex.getCoordinate());
boolean render = evRenderer.renderVertex(vertex, vvAttrs);
if (!render) continue;
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.
* @param attrs The edge visual attributes to fill-in.
* @return True to render this edge, false otherwise.
*/
boolean renderEdge(Edge e, EdgeVisualAttributes attrs);
/**
* @param v The vertex being rendered.
* @param attrs The vertex visual attributes to fill-in.
* @return True to render this vertex, false otherwise.
*/
boolean renderVertex(Vertex v, VertexVisualAttributes attrs);
/**
* 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 static class EdgeVisualAttributes {
public Color color;
public String label;
}
public static class VertexVisualAttributes {
public Color color;
public String label;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy