Package org.osm2world.core.world.modules

Source Code of org.osm2world.core.world.modules.RoadModule$Lane

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;
     
    }
   
  }
}
TOP

Related Classes of org.osm2world.core.world.modules.RoadModule$Lane

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.