package org.osm2world.core.world.modules; import static java.lang.Math.*; import static java.util.Arrays.asList; import static java.util.Collections.reverse; import static org.openstreetmap.josm.plugins.graphview.core.data.EmptyTagGroup.EMPTY_TAG_GROUP; import static org.openstreetmap.josm.plugins.graphview.core.util.ValueStringParser.parseOsmDecimal; import static org.osm2world.core.map_elevation.creation.EleConstraintEnforcer.ConstraintType.*; import static org.osm2world.core.math.GeometryUtil.interpolateElevation; import static org.osm2world.core.math.VectorXYZ.*; import static org.osm2world.core.target.common.material.Materials.*; import static org.osm2world.core.target.common.material.NamedTexCoordFunction.*; import static org.osm2world.core.target.common.material.TexCoordUtil.*; import static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.*; import static org.osm2world.core.world.modules.common.WorldModuleParseUtil.*; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.openstreetmap.josm.plugins.graphview.core.data.MapBasedTagGroup; import org.openstreetmap.josm.plugins.graphview.core.data.Tag; import org.openstreetmap.josm.plugins.graphview.core.data.TagGroup; import org.osm2world.core.map_data.data.MapArea; import org.osm2world.core.map_data.data.MapData; import org.osm2world.core.map_data.data.MapNode; import org.osm2world.core.map_data.data.MapWaySegment; import org.osm2world.core.map_elevation.creation.EleConstraintEnforcer; import org.osm2world.core.map_elevation.data.GroundState; import org.osm2world.core.math.GeometryUtil; import org.osm2world.core.math.PolygonXYZ; import org.osm2world.core.math.TriangleXYZ; import org.osm2world.core.math.VectorXYZ; import org.osm2world.core.math.VectorXZ; import org.osm2world.core.target.RenderableToAllTargets; import org.osm2world.core.target.Target; import org.osm2world.core.target.common.TextureData; import org.osm2world.core.target.common.material.Material; import org.osm2world.core.target.common.material.Materials; import org.osm2world.core.target.common.material.TexCoordFunction; import org.osm2world.core.world.data.TerrainBoundaryWorldObject; import org.osm2world.core.world.modules.common.ConfigurableWorldModule; import org.osm2world.core.world.network.AbstractNetworkWaySegmentWorldObject; import org.osm2world.core.world.network.JunctionNodeWorldObject; import org.osm2world.core.world.network.NetworkAreaWorldObject; import org.osm2world.core.world.network.VisibleConnectorNodeWorldObject; /** * adds roads to the world */ public class RoadModule extends ConfigurableWorldModule { /** determines whether right-hand or left-hand traffic is the default */ private static final boolean RIGHT_HAND_TRAFFIC_BY_DEFAULT = true; @Override public void applyTo(MapData grid) { for (MapWaySegment line : grid.getMapWaySegments()) { if (isRoad(line.getTags())) { line.addRepresentation(new Road(line, line.getTags())); } } for (MapArea area : grid.getMapAreas()) { if (isRoad(area.getTags())) { List<VectorXZ> coords = new ArrayList<VectorXZ>(); for (MapNode node : area.getBoundaryNodes()) { coords.add(node.getPos()); } coords.remove(coords.size()-1); area.addRepresentation(new RoadArea(area)); } } for (MapNode node : grid.getMapNodes()) { TagGroup tags = node.getOsmNode().tags; List<Road> connectedRoads = getConnectedRoads(node, false); if (connectedRoads.size() > 2) { node.addRepresentation(new RoadJunction(node)); } else if (connectedRoads.size() == 2 && tags.contains("highway", "crossing") && !tags.contains("crossing", "no")) { node.addRepresentation(new RoadCrossingAtConnector(node)); } else if (connectedRoads.size() == 2) { Road road1 = connectedRoads.get(0); Road road2 = connectedRoads.get(1); if (road1.getWidth() != road2.getWidth() /* TODO: || lane layouts not identical */) { node.addRepresentation(new RoadConnector(node)); } } } } private static boolean isRoad(TagGroup tags) { if (tags.containsKey("highway") && !tags.contains("highway", "construction") && !tags.contains("highway", "proposed")) { return true; } else { return tags.contains("railway", "platform") || tags.contains("leisure", "track"); } } private static boolean isSteps(TagGroup tags) { return tags.contains(new Tag("highway","steps")); } private static boolean isPath(TagGroup tags) { String highwayValue = tags.getValue("highway"); return "path".equals(highwayValue) || "footway".equals(highwayValue) || "cycleway".equals(highwayValue) || "bridleway".equals(highwayValue) || "steps".equals(highwayValue); } private static boolean isOneway(TagGroup tags) { return tags.contains("oneway", "yes") || (!tags.contains("oneway", "no") && (tags.contains("highway", "motorway") || (tags.contains("highway", "motorway_link")))); } private static int getDefaultLanes(TagGroup tags) { String highwayValue = tags.getValue("highway"); if (highwayValue == null || isPath(tags) || highwayValue.endsWith("_link") || "service".equals(highwayValue) || "track".equals(highwayValue) || "residential".equals(highwayValue) || "living_street".equals(highwayValue) || "pedestrian".equals(highwayValue) || "platform".equals(highwayValue)) { return 1; } else if ("motorway".equals(highwayValue)){ return 2; } else { return isOneway(tags) ? 1 : 2; } } /** * determines surface for a junction or connector/crossing. * If the node has an explicit surface tag, this is evaluated. * Otherwise, the result depends on the surface values of adjacent roads. */ private static Material getSurfaceForNode(MapNode node) { Material surface = getSurfaceMaterial( node.getTags().getValue("surface"), null); if (surface == null) { /* choose the surface of any adjacent road */ for (MapWaySegment segment : node.getConnectedWaySegments()) { if (segment.getPrimaryRepresentation() instanceof Road) { Road road = (Road)segment.getPrimaryRepresentation(); surface = road.getSurface(); break; } } } return surface; } private static Material getSurfaceForRoad(TagGroup tags, Material defaultSurface) { Material result; if (tags.containsKey("tracktype")) { if (tags.contains("tracktype", "grade1")) { result = ASPHALT; } else if (tags.contains("tracktype", "grade2")) { result = GRAVEL; } else { result = EARTH; } } else { result = defaultSurface; } return getSurfaceMaterial(tags.getValue("surface"), result); } private static Material getSurfaceMiddleForRoad(TagGroup tags, Material defaultSurface) { Material result; if (tags.contains("tracktype", "grade4") || tags.contains("tracktype", "grade5")) { result = TERRAIN_DEFAULT; // ideally, this would be the terrain type surrounds the track... } else { result = defaultSurface; } result = getSurfaceMaterial(tags.getValue("surface:middle"), result); if (result == GRASS) { result = TERRAIN_DEFAULT; } return result; } /** * returns all roads connected to a node * @param requireLanes only include roads that are not paths and have lanes */ private static List<Road> getConnectedRoads(MapNode node, boolean requireLanes) { List<Road> connectedRoadsWithLanes = new ArrayList<Road>(); for (MapWaySegment segment : node.getConnectedWaySegments()) { if (segment.getPrimaryRepresentation() instanceof Road) { Road road = (Road)segment.getPrimaryRepresentation(); if (!requireLanes || (road.getLaneLayout() != null && !isPath(road.tags))) { connectedRoadsWithLanes.add(road); } } } return connectedRoadsWithLanes; } /** * find matching lane pairs * (lanes that can be connected at a junction or connector) */ private static Map<Integer, Integer> findMatchingLanes( List<Lane> lanes1, List<Lane> lanes2, boolean isJunction, boolean isCrossing) { Map<Integer, Integer> matches = new HashMap<Integer, Integer>(); /* * iterate from inside to outside * (only for connectors, where it will lead to desirable connections * between straight motorcar lanes e.g. at highway exits) */ if (!isJunction) { for (int laneI = 0; laneI < lanes1.size() && laneI < lanes2.size(); ++laneI) { final Lane lane1 = lanes1.get(laneI); final Lane lane2 = lanes2.get(laneI); if (isCrossing && !lane1.type.isConnectableAtCrossings) { continue; } else if (isJunction && !lane1.type.isConnectableAtJunctions) { continue; } if (lane2.type.equals(lane1.type)) { matches.put(laneI, laneI); } } } /* iterate from outside to inside. * Mostly intended to gather sidewalks and other non-car lanes. */ for (int laneI = 0; laneI < lanes1.size() && laneI < lanes2.size(); ++laneI) { int lane1Index = lanes1.size() - 1 - laneI; int lane2Index = lanes2.size() - 1 - laneI; final Lane lane1 = lanes1.get(lane1Index); final Lane lane2 = lanes2.get(lane2Index); if (isCrossing && !lane1.type.isConnectableAtCrossings) { continue; } else if (isJunction && !lane1.type.isConnectableAtJunctions) { continue; } if (matches.containsKey(lane1Index) || matches.containsKey(lane2Index)) { continue; } if (lane2.type.equals(lane1.type)) { matches.put(lane1Index, lane2Index); } } return matches; } /** * determines connected lanes at a junction, crossing or connector */ private static List<LaneConnection> buildLaneConnections( MapNode node, boolean isJunction, boolean isCrossing) { List<Road> roads = getConnectedRoads(node, true); /* check whether the oneway special case applies */ if (isJunction) { boolean allOneway = true; int firstInboundIndex = -1; for (int i = 0; i < roads.size(); i++) { Road road = roads.get(i); if (!isOneway(road.tags)) { allOneway = false; break; } if (firstInboundIndex == -1 && road.segment.getEndNode() == node) { firstInboundIndex = i; } } if (firstInboundIndex != -1) { // sort into inbound and outbound oneways // (need to be sequential blocks in the road list) List<Road> inboundOnewayRoads = new ArrayList<Road>(); List<Road> outboundOnewayRoads = new ArrayList<Road>(); int i = 0; for (i = firstInboundIndex; i < roads.size(); i++) { if (roads.get(i).segment.getEndNode() != node) { break; //not inbound } inboundOnewayRoads.add(roads.get(i)); } reverse(inboundOnewayRoads); for (/* continue previous loop */; i % roads.size() != firstInboundIndex; i++) { outboundOnewayRoads.add(roads.get(i % roads.size())); } if (allOneway) { return buildLaneConnections_allOneway(node, inboundOnewayRoads, outboundOnewayRoads); } } } /* apply normal treatment (not oneway-specific) */ List<LaneConnection> result = new ArrayList<LaneConnection>(); for (int i = 0; i < roads.size(); i++) { final Road road1 = roads.get(i); final Road road2 = roads.get( (i+1) % roads.size()); addLaneConnectionsForRoadPair(result, node, road1, road2, isJunction, isCrossing); } return result; } /** * builds lane connections at a junction of just oneway roads. * Intended to handle motorway merges and splits well. * Inbound and outbound roads must not be mixed, * but build two separate continuous blocks instead. * * @param inboundOnewayRoadsLTR inbound roads, left to right * @param outboundOnewayRoadsLTR outbound roads, left to right */ private static List<LaneConnection> buildLaneConnections_allOneway( MapNode node, List<Road> inboundOnewayRoadsLTR, List<Road> outboundOnewayRoadsLTR) { List<Lane> inboundLanes = new ArrayList<Lane>(); List<Lane> outboundLanes = new ArrayList<Lane>(); for (Road road : inboundOnewayRoadsLTR) { inboundLanes.addAll(road.getLaneLayout().getLanesLeftToRight()); } for (Road road : outboundOnewayRoadsLTR) { outboundLanes.addAll(road.getLaneLayout().getLanesLeftToRight()); } Map<Integer, Integer> matches = findMatchingLanes(inboundLanes, outboundLanes, false, false); /* build connections */ List<LaneConnection> result = new ArrayList<RoadModule.LaneConnection>(); for (int lane1Index : matches.keySet()) { final Lane lane1 = inboundLanes.get(lane1Index); final Lane lane2 = outboundLanes.get(matches.get(lane1Index)); result.add(buildLaneConnection(lane1, lane2, RoadPart.LEFT, //TODO: road part is not always the same false, true)); } return result; } /** * determines connected lanes at a junction, crossing or connector * for a pair of two of the junction's roads. * Only connections between the left part of road1 with the right part of * road2 will be taken into account. */ private static void addLaneConnectionsForRoadPair( List<LaneConnection> result, MapNode node, Road road1, Road road2, boolean isJunction, boolean isCrossing) { /* get some basic info about the roads */ final boolean isRoad1Inbound = road1.segment.getEndNode() == node; final boolean isRoad2Inbound = road2.segment.getEndNode() == node; final List<Lane> lanes1, lanes2; lanes1 = road1.getLaneLayout().getLanes( isRoad1Inbound ? RoadPart.LEFT : RoadPart.RIGHT); lanes2 = road2.getLaneLayout().getLanes( isRoad2Inbound ? RoadPart.RIGHT : RoadPart.LEFT); /* determine which lanes are connected */ Map<Integer, Integer> matches = findMatchingLanes(lanes1, lanes2, isJunction, isCrossing); /* build the lane connections */ for (int lane1Index : matches.keySet()) { final Lane lane1 = lanes1.get(lane1Index); final Lane lane2 = lanes2.get(matches.get(lane1Index)); result.add(buildLaneConnection(lane1, lane2, RoadPart.LEFT, !isRoad1Inbound, !isRoad2Inbound)); } //TODO: connect "disappearing" lanes to a point on the other side // or draw caps (only for connectors) } private static LaneConnection buildLaneConnection( Lane lane1, Lane lane2, RoadPart roadPart, boolean atLane1Start, boolean atLane2Start) { List<VectorXYZ> leftLaneBorder = new ArrayList<VectorXYZ>(); leftLaneBorder.add(lane1.getBorderNode( atLane1Start, atLane1Start)); leftLaneBorder.add(lane2.getBorderNode( atLane2Start, !atLane2Start)); List<VectorXYZ> rightLaneBorder = new ArrayList<VectorXYZ>(); rightLaneBorder.add(lane1.getBorderNode( atLane1Start, !atLane1Start)); rightLaneBorder.add(lane2.getBorderNode( atLane2Start, atLane2Start)); return new LaneConnection(lane1.type, RoadPart.LEFT, lane1.road.rightHandTraffic, leftLaneBorder, rightLaneBorder); } /** * representation for junctions between roads. */ public static class RoadJunction extends JunctionNodeWorldObject implements RenderableToAllTargets, TerrainBoundaryWorldObject { public RoadJunction(MapNode node) { super(node); } @Override public void renderTo(Target<?> target) { Material material = getSurfaceForNode(node); Collection<TriangleXYZ> triangles = super.getTriangulation(); target.drawTriangles(material, triangles, triangleTexCoordLists(triangles, material, GLOBAL_X_Z)); /* connect some lanes such as sidewalks between adjacent roads */ List<LaneConnection> connections = buildLaneConnections( node, true, false); for (LaneConnection connection : connections) { connection.renderTo(target); } } @Override public GroundState getGroundState() { GroundState currentGroundState = null; checkEachLine: { for (MapWaySegment line : this.node.getConnectedWaySegments()) { if (line.getPrimaryRepresentation() == null) continue; GroundState lineGroundState = line.getPrimaryRepresentation().getGroundState(); if (currentGroundState == null) { currentGroundState = lineGroundState; } else if (currentGroundState != lineGroundState) { currentGroundState = GroundState.ON; break checkEachLine; } } } return currentGroundState; } } /* TODO: crossings at junctions - when there is, e.g., a footway connecting to the road! * (ideally, this would be implemented using more flexibly configurable * junctions which can have "preferred" segments that influence * the junction shape more/exclusively) */ /** * visible connectors where a road changes width or lane layout */ public static class RoadConnector extends VisibleConnectorNodeWorldObject implements RenderableToAllTargets, TerrainBoundaryWorldObject { private static final double MAX_CONNECTOR_LENGTH = 5; public RoadConnector(MapNode node) { super(node); } @Override public float getLength() { // length is at most a third of the shorter road segment's length List<Road> roads = getConnectedRoads(node, false); return (float)Math.min(Math.min( roads.get(0).segment.getLineSegment().getLength() / 3, roads.get(1).segment.getLineSegment().getLength() / 3), MAX_CONNECTOR_LENGTH); } @Override public void renderTo(Target<?> target) { List<LaneConnection> connections = buildLaneConnections( node, false, false); /* render connections */ for (LaneConnection connection : connections) { connection.renderTo(target); } /* render area not covered by connections */ //TODO: subtract area covered by connections Material material = getSurfaceForNode(node); Collection<TriangleXYZ> trianglesXYZ = getTriangulation(); target.drawTriangles(material, trianglesXYZ, triangleTexCoordLists(trianglesXYZ, material, GLOBAL_X_Z)); } @Override public GroundState getGroundState() { GroundState currentGroundState = null; checkEachLine: { for (MapWaySegment line : this.node.getConnectedWaySegments()) { if (line.getPrimaryRepresentation() == null) continue; GroundState lineGroundState = line.getPrimaryRepresentation().getGroundState(); if (currentGroundState == null) { currentGroundState = lineGroundState; } else if (currentGroundState != lineGroundState) { currentGroundState = GroundState.ON; break checkEachLine; } } } return currentGroundState; } } /** * representation for crossings (zebra crossing etc.) on roads */ public static class RoadCrossingAtConnector extends VisibleConnectorNodeWorldObject implements RenderableToAllTargets, TerrainBoundaryWorldObject { private static final float CROSSING_WIDTH = 3f; public RoadCrossingAtConnector(MapNode node) { super(node); } @Override public float getLength() { return parseWidth(node.getTags(), CROSSING_WIDTH); } @Override public void renderTo(Target<?> target) { VectorXYZ startLeft = getEleConnectors().getPosXYZ( startPos.subtract(cutVector.mult(0.5 * startWidth))); VectorXYZ startRight = getEleConnectors().getPosXYZ( startPos.add(cutVector.mult(0.5 * startWidth))); VectorXYZ endLeft = getEleConnectors().getPosXYZ( endPos.subtract(cutVector.mult(0.5 * endWidth))); VectorXYZ endRight = getEleConnectors().getPosXYZ( endPos.add(cutVector.mult(0.5 * endWidth))); /* determine surface material */ Material surface = getSurfaceForNode(node); if (node.getTags().contains("crossing", "zebra") || node.getTags().contains("crossing_ref", "zebra")) { surface = surface.withAddedLayers( ROAD_MARKING_ZEBRA.getTextureDataList()); } else if (!node.getTags().contains("crossing", "unmarked")) { surface = surface.withAddedLayers( ROAD_MARKING_CROSSING.getTextureDataList()); } /* draw crossing */ List<VectorXYZ> vs = asList(endLeft, startLeft, endRight, startRight); target.drawTriangleStrip(surface, vs, texCoordLists(vs, surface, GLOBAL_X_Z)); /* draw lane connections */ List<LaneConnection> connections = buildLaneConnections( node, false, true); for (LaneConnection connection : connections) { connection.renderTo(target); } } @Override public GroundState getGroundState() { GroundState currentGroundState = null; checkEachLine: { for (MapWaySegment line : this.node.getConnectedWaySegments()) { if (line.getPrimaryRepresentation() == null) continue; GroundState lineGroundState = line.getPrimaryRepresentation().getGroundState(); if (currentGroundState == null) { currentGroundState = lineGroundState; } else if (currentGroundState != lineGroundState) { currentGroundState = GroundState.ON; break checkEachLine; } } } return currentGroundState; } } /** representation of a road */ public static class Road extends AbstractNetworkWaySegmentWorldObject implements RenderableToAllTargets, TerrainBoundaryWorldObject { protected static final float DEFAULT_LANE_WIDTH = 3.5f; protected static final float DEFAULT_ROAD_CLEARING = 5; protected static final float DEFAULT_PATH_CLEARING = 2; protected static final List<VectorXYZ> HANDRAIL_SHAPE = asList( new VectorXYZ(-0.02f, -0.05f, 0), new VectorXYZ(-0.02f, 0f, 0), new VectorXYZ(+0.02f, 0f, 0), new VectorXYZ(+0.02f, -0.05f, 0)); public final boolean rightHandTraffic; public final LaneLayout laneLayout; public final float width; final private TagGroup tags; final public VectorXZ startCoord, endCoord; final private boolean steps; public Road(MapWaySegment line, TagGroup tags) { super(line); this.tags = tags; this.startCoord = line.getStartNode().getPos(); this.endCoord = line.getEndNode().getPos(); if (RIGHT_HAND_TRAFFIC_BY_DEFAULT) { if (tags.contains("driving_side", "left")) { rightHandTraffic = false; } else { rightHandTraffic = true; } } else { if (tags.contains("driving_side", "right")) { rightHandTraffic = true; } else { rightHandTraffic = false; } } this.steps = isSteps(tags); if (steps) { this.laneLayout = null; this.width = parseWidth(tags, 1.0f); } else { this.laneLayout = buildBasicLaneLayout(); this.width = calculateWidth(); laneLayout.setCalculatedValues(width); } } /** * creates a lane layout from several basic tags. */ private LaneLayout buildBasicLaneLayout() { boolean isOneway = isOneway(tags); /* determine which special lanes and attributes exist */ String divider = tags.getValue("divider"); String sidewalk = tags.containsKey("sidewalk") ? tags.getValue("sidewalk") : tags.getValue("footway"); boolean leftSidewalk = "left".equals(sidewalk) || "both".equals(sidewalk); boolean rightSidewalk = "right".equals(sidewalk) || "both".equals(sidewalk); boolean leftCycleway = tags.contains("cycleway:left", "lane") || tags.contains("cycleway", "lane"); boolean rightCycleway = tags.contains("cycleway:right", "lane") || tags.contains("cycleway", "lane"); /* get individual values for each lane */ TagGroup[] laneTagsRight = getPerLaneTags(RoadPart.RIGHT); TagGroup[] laneTagsLeft = getPerLaneTags(RoadPart.LEFT); /* determine the number of lanes */ Float lanes = null; if (tags.containsKey("lanes")) { lanes = parseOsmDecimal(tags.getValue("lanes"), false); } Float lanesRight = null; Float lanesLeft = null; //TODO handle oneway case String rightKey = rightHandTraffic ? "lanes:forward" : "lanes:backward"; if (laneTagsRight != null) { lanesRight = (float)laneTagsRight.length; } else if (tags.containsKey(rightKey)) { lanesRight = parseOsmDecimal(tags.getValue(rightKey), false); } String leftKey = rightHandTraffic ? "lanes:backward" : "lanes:forward"; if (laneTagsLeft != null) { lanesLeft = (float)laneTagsLeft.length; } else if (tags.containsKey(leftKey)) { lanesLeft = parseOsmDecimal(tags.getValue(leftKey), false); } int vehicleLaneCount; int vehicleLaneCountRight; int vehicleLaneCountLeft; if (lanesRight != null && lanesLeft != null) { vehicleLaneCountRight = (int)(float)lanesRight; vehicleLaneCountLeft = (int)(float)lanesLeft; vehicleLaneCount = vehicleLaneCountRight + vehicleLaneCountLeft; //TODO incorrect in case of center lanes } else { if (lanes == null) { vehicleLaneCount = getDefaultLanes(tags); } else { vehicleLaneCount = (int)(float) lanes; } if (lanesRight != null) { vehicleLaneCountRight = (int)(float)lanesRight; vehicleLaneCount = max(vehicleLaneCount, vehicleLaneCountRight); vehicleLaneCountLeft = vehicleLaneCount - vehicleLaneCountRight; } else if (lanesLeft != null) { vehicleLaneCountLeft = (int)(float)lanesLeft; vehicleLaneCount = max(vehicleLaneCount, vehicleLaneCountLeft); vehicleLaneCountRight = vehicleLaneCount - vehicleLaneCountLeft; } else { vehicleLaneCountLeft = vehicleLaneCount / 2; vehicleLaneCountRight = vehicleLaneCount - vehicleLaneCountLeft; } } /* create the layout */ LaneLayout layout = new LaneLayout(); // central divider if (vehicleLaneCountRight > 0 && vehicleLaneCountLeft > 0) { LaneType dividerType = DASHED_LINE; if ("dashed_line".equals(divider)) { dividerType = DASHED_LINE; } else if ("solid_line".equals(divider)) { dividerType = SOLID_LINE; } else if ("no".equals(divider)) { dividerType = null; } if (dividerType != null) { layout.getLanes(RoadPart.RIGHT).add(new Lane(this, dividerType, RoadPart.RIGHT, EMPTY_TAG_GROUP)); } } // left and right road part for (RoadPart roadPart : RoadPart.values()) { int lanesPart = (roadPart == RoadPart.RIGHT) ? vehicleLaneCountRight : vehicleLaneCountLeft; TagGroup[] laneTags = (roadPart == RoadPart.RIGHT) ? laneTagsRight : laneTagsLeft; for (int i = 0; i < lanesPart; ++ i) { if (i > 0) { // divider between lanes in the same direction layout.getLanes(roadPart).add(new Lane(this, DASHED_LINE, roadPart, EMPTY_TAG_GROUP)); } //lane itself TagGroup tags = (laneTags != null) ? laneTags[i] : EMPTY_TAG_GROUP; layout.getLanes(roadPart).add(new Lane(this, VEHICLE_LANE, roadPart, tags)); } } //special lanes if (leftCycleway) { layout.leftLanes.add(new Lane(this, CYCLEWAY, RoadPart.LEFT, EMPTY_TAG_GROUP)); } if (rightCycleway) { layout.rightLanes.add(new Lane(this, CYCLEWAY, RoadPart.RIGHT, EMPTY_TAG_GROUP)); } if (leftSidewalk) { layout.leftLanes.add(new Lane(this, KERB, RoadPart.LEFT, EMPTY_TAG_GROUP)); layout.leftLanes.add(new Lane(this, SIDEWALK, RoadPart.LEFT, EMPTY_TAG_GROUP)); } if (rightSidewalk) { layout.rightLanes.add(new Lane(this, KERB, RoadPart.RIGHT, EMPTY_TAG_GROUP)); layout.rightLanes.add(new Lane(this, SIDEWALK, RoadPart.RIGHT, EMPTY_TAG_GROUP)); } return layout; } /** * evaluates tags using the :lanes key suffix * * @return array with values; null if the tag isn't used */ @SuppressWarnings("unchecked") private TagGroup[] getPerLaneTags(RoadPart roadPart) { /* determine which of the suffixes :lanes[:forward|:backward] matter */ List<String> relevantSuffixes; if (roadPart == RoadPart.RIGHT ^ !rightHandTraffic) { // the forward part if (isOneway(tags)) { relevantSuffixes = asList(":lanes", ":lanes:forward"); } else { relevantSuffixes = asList(":lanes:forward"); } } else { // the backward part relevantSuffixes = asList(":lanes:backward"); } /* evaluate tags with one of the relevant suffixes */ Map<String, String>[] resultMaps = null; for (String suffix : relevantSuffixes) { for (Tag tag : tags) { if (tag.key.endsWith(suffix)) { String baseKey = tag.key.substring(0, tag.key.lastIndexOf(suffix)); String[] values = tag.value.split("\\|"); if (resultMaps == null) { resultMaps = new Map[values.length]; for (int i = 0; i < resultMaps.length; i++) { resultMaps[i] = new HashMap<String, String>(); } } else if (values.length != resultMaps.length) { // inconsistent number of lanes return null; } for (int i = 0; i < values.length; i++) { resultMaps[i].put(baseKey, values[i].trim()); } } } } /* build a TagGroup for each lane from the result */ if (resultMaps == null) { return null; } else { TagGroup[] result = new TagGroup[resultMaps.length]; for (int i = 0; i < resultMaps.length; i++) { result[i] = new MapBasedTagGroup(resultMaps[i]); } return result; } } private float calculateWidth() { // if the width of all lanes is known, use the sum as the road's width Float sumWidth = calculateLaneBasedWidth(false, false); if (sumWidth != null) return sumWidth; // if the width of the road is explicitly tagged, use that value // (note that this has lower priority than the sum of lane widths, // to avoid errors when the two values don't match) float explicitWidth = parseWidth(tags, -1); if (explicitWidth != -1) return explicitWidth; // if there is some basic info on lanes, use that if (tags.containsKey("lanes") || tags.containsKey("divider")) { return calculateLaneBasedWidth(true, false); } // if all else fails, make a guess return calculateLaneBasedWidth(true, true) + estimateVehicleLanesWidth(); } /** * calculates the width of the road as the sum of the widths * of its lanes * * @param useDefaults whether to use a default for unknown widths * @param ignoreVehicleLanes ignoring full-width lanes, * which means that only sidewalks, cycleways etc. will be counted * * @return the estimated width, or null if a lane has unknown width * and no defaults are permitted */ private Float calculateLaneBasedWidth(boolean useDefaults, boolean ignoreVehicleLanes) { float width = 0; for (Lane lane : laneLayout.getLanesLeftToRight()) { if (lane.type == VEHICLE_LANE && ignoreVehicleLanes) continue; if (lane.getAbsoluteWidth() == null) { if (useDefaults) { width += DEFAULT_LANE_WIDTH; } else { return null; } } else { width += lane.getAbsoluteWidth(); } } return width; } /** * calculates a rough estimate of the road's vehicle lanes' total width * based on road type and oneway */ private float estimateVehicleLanesWidth() { String highwayValue = tags.getValue("highway"); float width = 0; /* guess the combined width of all vehicle lanes */ if (!tags.containsKey("lanes") && !tags.containsKey("divider")) { if (isPath(tags)) { width = 1f; } else if ("service".equals(highwayValue) || "track".equals(highwayValue)) { if (tags.contains("service", "parking_aisle")) { width = DEFAULT_LANE_WIDTH * 0.8f; } else { width = DEFAULT_LANE_WIDTH; } } else if ("primary".equals(highwayValue) || "secondary".equals(highwayValue)) { width = 2 * DEFAULT_LANE_WIDTH; } else if ("motorway".equals(highwayValue)) { width = 2.5f * DEFAULT_LANE_WIDTH; } else if (tags.containsKey("oneway") && !tags.getValue("oneway").equals("no")) { width = DEFAULT_LANE_WIDTH; } else { width = 4; } } return width; } @Override public void defineEleConstraints(EleConstraintEnforcer enforcer) { super.defineEleConstraints(enforcer); /* impose sensible maximum incline (35% is "the world's steepest residential street") */ if (!isPath(tags) && !isSteps(tags) && !tags.containsKey("incline")) { enforcer.requireIncline(MAX, +0.35, getCenterlineEleConnectors()); enforcer.requireIncline(MIN, -0.35, getCenterlineEleConnectors()); } } @Override public float getWidth() { return width; } public Material getSurface() { return getSurfaceForRoad(tags, ASPHALT); } public LaneLayout getLaneLayout() { return laneLayout; } private void renderStepsTo(Target<?> target) { final VectorXZ startWithOffset = getStartPosition(); final VectorXZ endWithOffset = getEndPosition(); List<VectorXYZ> leftOutline = getOutline(false); List<VectorXYZ> rightOutline = getOutline(true); double lineLength = VectorXZ.distance ( segment.getStartNode().getPos(), segment.getEndNode().getPos()); /* render ground first (so gaps between the steps look better) */ List<VectorXYZ> vs = createTriangleStripBetween( leftOutline, rightOutline); target.drawTriangleStrip(ASPHALT, vs, texCoordLists(vs, ASPHALT, GLOBAL_X_Z)); /* determine the length of each individual step */ float stepLength = 0.3f; if (tags.containsKey("step_count")) { try { int stepCount = Integer.parseInt(tags.getValue("step_count")); stepLength = (float)lineLength / stepCount; } catch (NumberFormatException e) { /* don't overwrite default length */ } } /* locate the position on the line at the beginning/end of each step * (positions on the line spaced by step length), * interpolate heights between adjacent points with elevation */ List<VectorXYZ> centerline = getCenterline(); List<VectorXZ> stepBorderPositionsXZ = GeometryUtil.equallyDistributePointsAlong( stepLength, true, startWithOffset, endWithOffset); List<VectorXYZ> stepBorderPositions = new ArrayList<VectorXYZ>(); for (VectorXZ posXZ : stepBorderPositionsXZ) { VectorXYZ posXYZ = interpolateElevation(posXZ, centerline.get(0), centerline.get(centerline.size() - 1)); stepBorderPositions.add(posXYZ); } /* draw steps */ for (int step = 0; step < stepBorderPositions.size() - 1; step++) { VectorXYZ frontCenter = stepBorderPositions.get(step); VectorXYZ backCenter = stepBorderPositions.get(step+1); double height = abs(frontCenter.y - backCenter.y); VectorXYZ center = (frontCenter.add(backCenter)).mult(0.5); center = center.subtract(Y_UNIT.mult(0.5 * height)); VectorXZ faceDirection = segment.getDirection(); if (frontCenter.y < backCenter.y) { //invert if upwards faceDirection = faceDirection.invert(); } target.drawBox(Materials.STEPS_DEFAULT, center, faceDirection, height, width, backCenter.distanceToXZ(frontCenter)); } /* draw handrails */ List<List<VectorXYZ>> handrailFootprints = new ArrayList<List<VectorXYZ>>(); if (segment.getTags().contains("handrail:left","yes")) { handrailFootprints.add(leftOutline); } if (segment.getTags().contains("handrail:right","yes")) { handrailFootprints.add(rightOutline); } int centerHandrails = 0; if (segment.getTags().contains("handrail:center","yes")) { centerHandrails = 1; } else if (segment.getTags().containsKey("handrail:center")) { try { centerHandrails = Integer.parseInt( segment.getTags().getValue("handrail:center")); } catch (NumberFormatException e) {} } for (int i = 0; i < centerHandrails; i++) { handrailFootprints.add(createLineBetween( leftOutline, rightOutline, (i + 1.0f) / (centerHandrails + 1))); } for (List<VectorXYZ> handrailFootprint : handrailFootprints) { List<VectorXYZ> handrailLine = new ArrayList<VectorXYZ>(); for (VectorXYZ v : handrailFootprint) { handrailLine.add(v.y(v.y + 1)); } List<List<VectorXYZ>> strips = createShapeExtrusionAlong( HANDRAIL_SHAPE, handrailLine, Collections.nCopies(handrailLine.size(), VectorXYZ.Y_UNIT)); for (List<VectorXYZ> strip : strips) { target.drawTriangleStrip(HANDRAIL_DEFAULT, strip, texCoordLists(strip, HANDRAIL_DEFAULT, STRIP_WALL)); } target.drawColumn(HANDRAIL_DEFAULT, 4, handrailFootprint.get(0), 1, 0.03, 0.03, false, true); target.drawColumn(HANDRAIL_DEFAULT, 4, handrailFootprint.get(handrailFootprint.size()-1), 1, 0.03, 0.03, false, true); } } private void renderLanesTo(Target<?> target) { List<Lane> lanesLeftToRight = laneLayout.getLanesLeftToRight(); /* draw lanes themselves */ for (Lane lane : lanesLeftToRight) { lane.renderTo(target); } /* close height gaps at left and right border of the road */ Lane firstLane = lanesLeftToRight.get(0); Lane lastLane = lanesLeftToRight.get(lanesLeftToRight.size() - 1); if (firstLane.getHeightAboveRoad() > 0) { List<VectorXYZ> vs = createTriangleStripBetween( getOutline(false), addYList(getOutline(false), firstLane.getHeightAboveRoad())); target.drawTriangleStrip(getSurface(), vs, texCoordLists(vs, getSurface(), STRIP_WALL)); } if (lastLane.getHeightAboveRoad() > 0) { List<VectorXYZ> vs = createTriangleStripBetween( addYList(getOutline(true), lastLane.getHeightAboveRoad()), getOutline(true)); target.drawTriangleStrip(getSurface(), vs, texCoordLists(vs, getSurface(), STRIP_WALL)); } } @Override public void renderTo(Target<?> target) { if (steps) { renderStepsTo(target); } else { renderLanesTo(target); } } } public static class RoadArea extends NetworkAreaWorldObject implements RenderableToAllTargets, TerrainBoundaryWorldObject { private static final float DEFAULT_CLEARING = 5f; public RoadArea(MapArea area) { super(area); } @Override public void renderTo(Target<?> target) { String surface = area.getTags().getValue("surface"); Material material = getSurfaceMaterial(surface, ASPHALT); Collection<TriangleXYZ> triangles = getTriangulation(); target.drawTriangles(material, triangles, triangleTexCoordLists(triangles, material, GLOBAL_X_Z)); } @Override public GroundState getGroundState() { if (BridgeModule.isBridge(area.getTags())) { return GroundState.ABOVE; } else if (TunnelModule.isTunnel(area.getTags())) { return GroundState.BELOW; } else { return GroundState.ON; } } } private static enum RoadPart { LEFT, RIGHT //TODO add CENTRE lane support } private static class LaneLayout { public final List<Lane> leftLanes = new ArrayList<Lane>(); public final List<Lane> rightLanes = new ArrayList<Lane>(); public List<Lane> getLanes(RoadPart roadPart) { switch (roadPart) { case LEFT: return leftLanes; case RIGHT: return rightLanes; default: throw new Error("unhandled road part value"); } } public List<Lane> getLanesLeftToRight() { List<Lane> result = new ArrayList<Lane>(); result.addAll(leftLanes); Collections.reverse(result); result.addAll(rightLanes); return result; } /** * calculates and sets all lane attributes * that are not known during lane creation */ public void setCalculatedValues(double totalRoadWidth) { /* determine width of lanes without explicitly assigned width */ int lanesWithImplicitWidth = 0; double remainingWidth = totalRoadWidth; for (RoadPart part : RoadPart.values()) { for (Lane lane : getLanes(part)) { if (lane.getAbsoluteWidth() == null) { lanesWithImplicitWidth += 1; } else { remainingWidth -= lane.getAbsoluteWidth(); } } } double implicitLaneWidth = remainingWidth / lanesWithImplicitWidth; /* calculate a factor to reduce all lanes' width * if the sum of their widths would otherwise * be larger than that of the road */ double laneWidthScaling = 1.0; if (remainingWidth < 0) { double widthSum = totalRoadWidth - remainingWidth; implicitLaneWidth = 1; widthSum += lanesWithImplicitWidth * implicitLaneWidth; laneWidthScaling = totalRoadWidth / widthSum; } /* assign values */ for (RoadPart part : asList(RoadPart.LEFT, RoadPart.RIGHT)) { double heightAboveRoad = 0; for (Lane lane : getLanes(part)) { double relativeWidth; if (lane.getAbsoluteWidth() == null) { relativeWidth = laneWidthScaling * (implicitLaneWidth / totalRoadWidth); } else { relativeWidth = laneWidthScaling * (lane.getAbsoluteWidth() / totalRoadWidth); } lane.setCalculatedValues1(relativeWidth, heightAboveRoad); heightAboveRoad += lane.getHeightOffset(); } } /* calculate relative lane positions based on relative width */ double accumulatedWidth = 0; for (Lane lane : getLanesLeftToRight()) { double relativePositionLeft = accumulatedWidth; accumulatedWidth += lane.getRelativeWidth(); double relativePositionRight = accumulatedWidth; if (relativePositionRight > 1) { //avoids precision problems relativePositionRight = 1; } lane.setCalculatedValues2(relativePositionLeft, relativePositionRight); } } /** * calculates and sets all lane attributes * that are not known during lane creation */ } /** * a lane or lane divider of the road segment. * * Field values depend on neighboring lanes and are therefore calculated * and defined in two phases. Results are then set using * {@link #setCalculatedValues1(double, double)} and * {@link #setCalculatedValues2(double, double)}, respectively. */ private static final class Lane implements RenderableToAllTargets { public final Road road; public final LaneType type; public final RoadPart roadPart; public final TagGroup laneTags; private int phase = 0; private double relativeWidth; private double heightAboveRoad; private double relativePositionLeft; private double relativePositionRight; public Lane(Road road, LaneType type, RoadPart roadPart, TagGroup laneTags) { this.road = road; this.type = type; this.roadPart = roadPart; this.laneTags = laneTags; } /** returns width in meters or null for undefined width */ public Double getAbsoluteWidth() { return type.getAbsoluteWidth(road.tags, laneTags); } /** returns height increase relative to inner neighbor */ public double getHeightOffset() { return type.getHeightOffset(road.tags, laneTags); } public void setCalculatedValues1(double relativeWidth, double heightAboveRoad) { assert phase == 0; this.relativeWidth = relativeWidth; this.heightAboveRoad = heightAboveRoad; phase = 1; } public void setCalculatedValues2(double relativePositionLeft, double relativePositionRight) { assert phase == 1; this.relativePositionLeft = relativePositionLeft; this.relativePositionRight = relativePositionRight; phase = 2; } public Double getRelativeWidth() { assert phase > 0; return relativeWidth; } public double getHeightAboveRoad() { assert phase > 0; return heightAboveRoad; } /** * provides access to the first and last node * of the lane's left and right border */ public VectorXYZ getBorderNode(boolean start, boolean right) { assert phase > 1; double relativePosition = right ? relativePositionRight : relativePositionLeft; if (relativePosition < 0 || relativePosition > 1) { System.out.println("PROBLEM"); } VectorXYZ roadPoint = road.getPointOnCut(start, relativePosition); return roadPoint.add(0, getHeightAboveRoad(), 0); } public void renderTo(Target<?> target) { assert phase > 1; if (road.isBroken()) return; List<VectorXYZ> leftLaneBorder = createLineBetween( road.getOutline(false), road.getOutline(true), (float)relativePositionLeft); leftLaneBorder = addYList(leftLaneBorder, getHeightAboveRoad()); List<VectorXYZ> rightLaneBorder = createLineBetween( road.getOutline(false), road.getOutline(true), (float)relativePositionRight); rightLaneBorder = addYList(rightLaneBorder, getHeightAboveRoad()); type.render(target, roadPart, road.rightHandTraffic, road.tags, laneTags, leftLaneBorder, rightLaneBorder); } @Override public String toString() { return "{" + type + ", " + roadPart + "}"; } } /** * a connection between two lanes (e.g. at a junction) */ private static class LaneConnection implements RenderableToAllTargets { public final LaneType type; public final RoadPart roadPart; public final boolean rightHandTraffic; private final List<VectorXYZ> leftBorder; private final List<VectorXYZ> rightBorder; private LaneConnection(LaneType type, RoadPart roadPart, boolean rightHandTraffic, List<VectorXYZ> leftBorder, List<VectorXYZ> rightBorder) { this.type = type; this.roadPart = roadPart; this.rightHandTraffic = rightHandTraffic; this.leftBorder = leftBorder; this.rightBorder = rightBorder; } /** * returns the outline of this connection. * For determining the total terrain covered by junctions and connectors. */ public PolygonXYZ getOutline() { List<VectorXYZ> outline = new ArrayList<VectorXYZ>(); outline.addAll(leftBorder); List<VectorXYZ>rOutline = new ArrayList<VectorXYZ>(rightBorder); Collections.reverse(rOutline); outline.addAll(rOutline); outline.add(outline.get(0)); return new PolygonXYZ(outline); } public void renderTo(Target<?> target) { type.render(target, roadPart, rightHandTraffic, EMPTY_TAG_GROUP, EMPTY_TAG_GROUP, leftBorder, rightBorder); } } /** * a type of lanes. Determines visual appearance, * and contains the intelligence for evaluating type-specific tags. */ private static abstract class LaneType { private final String typeName; public final boolean isConnectableAtCrossings; public final boolean isConnectableAtJunctions; private LaneType(String typeName, boolean isConnectableAtCrossings, boolean isConnectableAtJunctions) { this.typeName = typeName; this.isConnectableAtCrossings = isConnectableAtCrossings; this.isConnectableAtJunctions = isConnectableAtJunctions; } public abstract void render(Target<?> target, RoadPart roadPart, boolean rightHandTraffic, TagGroup roadTags, TagGroup laneTags, List<VectorXYZ> leftLaneBorder, List<VectorXYZ> rightLaneBorder); public abstract Double getAbsoluteWidth( TagGroup roadTags, TagGroup laneTags); public abstract double getHeightOffset( TagGroup roadTags, TagGroup laneTags); @Override public String toString() { return typeName; } } private static abstract class FlatTexturedLane extends LaneType { private FlatTexturedLane(String typeName, boolean isConnectableAtCrossings, boolean isConnectableAtJunctions) { super(typeName, isConnectableAtCrossings, isConnectableAtJunctions); } @Override public void render(Target<?> target, RoadPart roadPart, boolean rightHandTraffic, TagGroup roadTags, TagGroup laneTags, List<VectorXYZ> leftLaneBorder, List<VectorXYZ> rightLaneBorder) { Material surface = getSurface(roadTags, laneTags); Material surfaceMiddle = getSurfaceMiddle(roadTags, laneTags); /* draw lane triangle strips */ if (surfaceMiddle == null || surfaceMiddle.equals(surface)) { List<VectorXYZ> vs = createTriangleStripBetween( leftLaneBorder, rightLaneBorder); boolean mirrorLeftRight = laneTags.containsKey("turn") && laneTags.getValue("turn").contains("left"); if (!roadTags.contains("highway", "motorway")) { surface = addTurnArrows(surface, laneTags); } target.drawTriangleStrip(surface, vs, texCoordLists(vs, surface, new ArrowTexCoordFunction( roadPart, rightHandTraffic, mirrorLeftRight))); } else { List<VectorXYZ> leftMiddleBorder = createLineBetween(leftLaneBorder, rightLaneBorder, 0.3f); List<VectorXYZ> rightMiddleBorder = createLineBetween(leftLaneBorder, rightLaneBorder, 0.7f); List<VectorXYZ> vsLeft = createTriangleStripBetween( leftLaneBorder, leftMiddleBorder); List<VectorXYZ> vsMiddle = createTriangleStripBetween( leftMiddleBorder, rightMiddleBorder); List<VectorXYZ> vsRight = createTriangleStripBetween( rightMiddleBorder, rightLaneBorder); target.drawTriangleStrip(surface, vsLeft, texCoordLists(vsLeft, surface, GLOBAL_X_Z)); target.drawTriangleStrip(surfaceMiddle, vsMiddle, texCoordLists(vsMiddle, surfaceMiddle, GLOBAL_X_Z)); target.drawTriangleStrip(surface, vsRight, texCoordLists(vsRight, surface, GLOBAL_X_Z)); } } @Override public double getHeightOffset(TagGroup roadTags, TagGroup laneTags) { return 0; } protected Material getSurface(TagGroup roadTags, TagGroup laneTags) { return getSurfaceMaterial(laneTags.getValue("surface"), getSurfaceForRoad(roadTags, ASPHALT)); } protected Material getSurfaceMiddle(TagGroup roadTags, TagGroup laneTags) { return getSurfaceMaterial(laneTags.getValue("surface:middle"), getSurfaceMiddleForRoad(roadTags, null)); } } private static final LaneType VEHICLE_LANE = new FlatTexturedLane( "VEHICLE_LANE", false, false) { public Double getAbsoluteWidth(TagGroup roadTags, TagGroup laneTags) { double width = parseWidth(laneTags, -1); if (width == -1) { return null; } else { return width; } } }; private static final LaneType CYCLEWAY = new FlatTexturedLane( "CYCLEWAY", false, false) { public Double getAbsoluteWidth(TagGroup roadTags, TagGroup laneTags) { return (double)parseWidth(laneTags, 0.5f); } @Override protected Material getSurface(TagGroup roadTags, TagGroup laneTags) { Material material = super.getSurface(roadTags, laneTags); if (material == ASPHALT) return RED_ROAD_MARKING; else return material; } }; private static final LaneType SIDEWALK = new FlatTexturedLane( "SIDEWALK", true, true) { public Double getAbsoluteWidth(TagGroup roadTags, TagGroup laneTags) { return (double)parseWidth(laneTags, 1.0f); } }; private static final LaneType SOLID_LINE = new FlatTexturedLane( "SOLID_LINE", false, false) { @Override public Double getAbsoluteWidth(TagGroup roadTags, TagGroup laneTags) { return (double)parseWidth(laneTags, 0.1f); } @Override protected Material getSurface(TagGroup roadTags, TagGroup laneTags) { return ROAD_MARKING; } }; private static final LaneType DASHED_LINE = new FlatTexturedLane( "DASHED_LINE", false, false) { @Override public Double getAbsoluteWidth(TagGroup roadTags, TagGroup laneTags) { return (double)parseWidth(laneTags, 0.1f); } @Override protected Material getSurface(TagGroup roadTags, TagGroup laneTags) { return ROAD_MARKING_DASHED; } }; private static final LaneType KERB = new LaneType( "KERB", true, true) { @Override public void render(Target<?> target, RoadPart roadPart, boolean rightHandTraffic, TagGroup roadTags, TagGroup laneTags, List<VectorXYZ> leftLaneBorder, List<VectorXYZ> rightLaneBorder) { List<VectorXYZ> border1, border2, border3; double height = getHeightOffset(roadTags, laneTags); if (roadPart == RoadPart.LEFT) { border1 = addYList(leftLaneBorder, height); border2 = addYList(rightLaneBorder, height); border3 = rightLaneBorder; } else { border1 = leftLaneBorder; border2 = addYList(leftLaneBorder, height); border3 = addYList(rightLaneBorder, height); } List<VectorXYZ> vs1_2 = createTriangleStripBetween( border1, border2); target.drawTriangleStrip(Materials.KERB, vs1_2, texCoordLists(vs1_2, Materials.KERB, STRIP_FIT_HEIGHT)); List<VectorXYZ> vs2_3 = createTriangleStripBetween( border2, border3); target.drawTriangleStrip(Materials.KERB, vs2_3, texCoordLists(vs2_3, Materials.KERB, STRIP_FIT_HEIGHT)); } @Override public Double getAbsoluteWidth(TagGroup roadTags, TagGroup laneTags) { return (double)parseWidth(laneTags, 0.15f); } @Override public double getHeightOffset(TagGroup roadTags, TagGroup laneTags) { return (double)parseHeight(laneTags, 0.12f); } }; /** * adds a texture layer for turn arrows (if any) to a material * * @return a material based on the input, possibly with added turn arrows */ private static Material addTurnArrows(Material material, TagGroup laneTags) { Material arrowMaterial = null; /* find the right material */ String turn = laneTags.getValue("turn"); if (turn != null) { if (turn.contains("through") && turn.contains("right")) { arrowMaterial = ROAD_MARKING_ARROW_THROUGH_RIGHT; } else if (turn.contains("through") && turn.contains("left")) { arrowMaterial = ROAD_MARKING_ARROW_THROUGH_RIGHT; } else if (turn.contains("through")) { arrowMaterial = ROAD_MARKING_ARROW_THROUGH; } else if (turn.contains("right") && turn.contains("left")) { arrowMaterial = ROAD_MARKING_ARROW_RIGHT_LEFT; } else if (turn.contains("right")) { arrowMaterial = ROAD_MARKING_ARROW_RIGHT; } else if (turn.contains("left")) { arrowMaterial = ROAD_MARKING_ARROW_RIGHT; } } /* apply the results */ if (arrowMaterial != null) { material = material.withAddedLayers(arrowMaterial.getTextureDataList()); } return material; } /** * a texture coordinate function for arrow road markings on turn lanes. * Has special features including centering the arrow, placing it at an * offset from the end of the road, and taking available space into account. * * To reduce the number of necessary textures, it uses mirrored versions of * the various right-pointing arrows for left-pointing arrows. */ private static class ArrowTexCoordFunction implements TexCoordFunction { private final RoadPart roadPart; private final boolean rightHandTraffic; private final boolean mirrorLeftRight; private ArrowTexCoordFunction(RoadPart roadPart, boolean rightHandTraffic, boolean mirrorLeftRight) { this.roadPart = roadPart; this.rightHandTraffic = rightHandTraffic; this.mirrorLeftRight = mirrorLeftRight; } @Override public List<VectorXZ> apply(List<VectorXYZ> vs, TextureData textureData) { if (vs.size() % 2 == 1) { throw new IllegalArgumentException("not a triangle strip lane"); } List<VectorXZ> result = new ArrayList<VectorXZ>(vs.size()); boolean forward = roadPart == RoadPart.LEFT ^ rightHandTraffic; /* calculate length of the lane */ double totalLength = 0; for (int i = 0; i+2 < vs.size(); i += 2) { totalLength += vs.get(i).distanceToXZ(vs.get(i+2)); } /* calculate texture coordinate list */ double accumulatedLength = forward ? totalLength : 0; for (int i = 0; i < vs.size(); i++) { VectorXYZ v = vs.get(i); // increase accumulated length after every second vector if (i > 0 && i % 2 == 0) { double segmentLength = v.xz().distanceTo(vs.get(i-2).xz()); if (forward) { accumulatedLength -= segmentLength; } else { accumulatedLength += segmentLength; } } // determine width of the lane at that point double width = (i % 2 == 0) ? v.distanceTo(vs.get(i+1)) : v.distanceTo(vs.get(i-1)); // determine whether this vertex should get the higher or // lower t coordinate from the vertex pair boolean higher = i % 2 == 0; if (!forward) { higher = !higher; } if (mirrorLeftRight) { higher = !higher; } // calculate texture coords double s, t; s = accumulatedLength / textureData.width; if (width > textureData.height) { double padding = ((width / textureData.height) - 1) / 2; t = higher ? 0 - padding : 1 + padding; } else { t = higher ? 0 : 1; } result.add(new VectorXZ(s, t)); } return result; } } }