pyoscx / scenariogeneration

Python library to generate linked OpenDRIVE and OpenSCENARIO files
Mozilla Public License 2.0
276 stars 86 forks source link

Linking junction connecting roads with create_lane_links_from_ids() #172

Open johschmitz opened 1 year ago

johschmitz commented 1 year ago

I am trying to build a better junction connecting road tool for my Blender addon and I am now facing this issue:

scenariogeneration/xodr/links.py", line 728, in create_lane_links_from_ids
    raise NotImplementedError(
NotImplementedError: This API currently does not support linking with junction connecting roads.

My current approach is to build one road with a single lane for each connection inside the junction because that simplifies the tool a lot. Do you think it would be a lot of work to support this use case?

MandolinMicke commented 1 year ago

Hmm, have to look into this (on vacation now though), but I do something similar in the junctioncreator I think?

johschmitz commented 1 year ago

There seems to be an internal API for this: https://github.com/pyoscx/scenariogeneration/blob/2320db95e1ad0e826e661789ff65db70eeae0c5a/scenariogeneration/xodr/generators.py#L948

Edit: This is only for the connection XML element in the junction XML element not for the link in the road XML element.

johschmitz commented 1 year ago

I tried to solve this now based on this example: https://github.com/pyoscx/scenariogeneration/blob/main/examples/xodr/junction_trippel_twoway.py However, it does not work for me because some of the lane level linking of the junction connecting roads is implicitly done in odr.adjust_roads_and_lanes(). So I am now back to the same problem and it seems there is now way of solving this problem without using internal APIs :(

johschmitz commented 1 year ago

Just for better understanding, here is a screenshot of the connecting road I would like to create:

image

My code looks like this

    # Create a junction connecting road
    # TODO for now we use a single spiral, later we should use spiral - arc - spiral
    planview = xodr.PlanView()
    planview.set_start_point(obj_jcr['geometry'][0]['point_start'][0],
        obj_jcr['geometry'][0]['point_start'][1],obj_jcr['geometry'][0]['heading_start'])
    geometry = xodr.Spiral(obj_jcr['geometry'][0]['curvature_start'],
        obj_jcr['geometry'][0]['curvature_end'], length=obj_jcr['geometry'][0]['length'])
    planview.add_geometry(geometry)
    lanes = self.create_lanes(obj_jcr)
    connecting_road = xodr.Road(obj_jcr['id_odr'],planview,lanes, road_type=junction_id)
    self.add_elevation_profiles(obj_jcr, connecting_road)

    # Incoming road
    incoming_road = self.get_road_by_id(roads, obj_jcr['link_predecessor_id_l'])
    contact_point = mapping_contact_point[obj_jcr['link_predecessor_cp_l']]
    connecting_road.add_predecessor(xodr.ElementType.road, incoming_road.id, contact_point)
    lane_id_incoming = obj_jcr['id_lane_predecessor']

    # Outgoing road
    outgoing_road = self.get_road_by_id(roads, obj_jcr['link_successor_id_l'])
    contact_point = mapping_contact_point[obj_jcr['link_successor_cp_l']]
    connecting_road.add_successor(xodr.ElementType.road, outgoing_road.id, contact_point)
    lane_id_outgoing = obj_jcr['id_lane_successor']

    # Lane linking for the junction connection elements
    if obj_jcr['lanes_left_num'] == 1:
        lane_id_connecting = 1
    elif obj_jcr['lanes_right_num'] == 1:
        lane_id_connecting = -1
    else:
        self.report({'ERROR'}, 'Only single lane connecting roads are supported!')
    connection = xodr.Connection(incoming_road.id, connecting_road.id, xodr.ContactPoint.start)
    connection.add_lanelink(lane_id_incoming, lane_id_connecting)
    junction.add_connection(connection)
    connection = xodr.Connection(outgoing_road.id, connecting_road.id, xodr.ContactPoint.end)
    connection.add_lanelink(lane_id_outgoing, lane_id_connecting)
    junction.add_connection(connection)

    # Junction connecting roads also need to be registered as "normal" roads
    odr.add_road(connecting_road)

But the links in the connecting road are missing:

...
    <road rule="RHT" id="6" junction="3" length="20.422447048616192">
        <link>
            <predecessor elementType="road" elementId="1" contactPoint="end"/>
            <successor elementType="road" elementId="2" contactPoint="start"/>
        </link>
        ...
        <lanes>
            <laneSection s="0">
                <center>
                    <lane id="0" type="none" level="false">
                        <roadMark sOffset="0" type="none" weight="standard" color="standard" height="0.02"/>
                    </lane>
                </center>
                <right>
                    <lane id="-1" type="driving" level="false">
                        <link/>
                        <width a="3.75" b="0.0" c="0.0" d="0.0" sOffset="0"/>
                        <roadMark sOffset="0" type="none" weight="standard" color="standard" height="0.02"/>
                    </lane>
                </right>
            </laneSection>
        </lanes>
    </road>
    <junction name="junction_3" id="3" type="default">
        <connection incomingRoad="1" id="0" contactPoint="start" connectingRoad="6">
            <laneLink from="-4" to="-1"/>
        </connection>
        <connection incomingRoad="2" id="1" contactPoint="end" connectingRoad="6">
            <laneLink from="-4" to="-1"/>
        </connection>
    </junction>
...

So I need to add something like

    xodr.create_lane_links_from_ids(incoming_road, connecting_road, [lane_id_incoming], [lane_id_connecting])
    xodr.create_lane_links_from_ids(connecting_road, outgoing_road, [lane_id_connecting], [lane_id_outgoing])
johschmitz commented 1 year ago

I checked what is missing to implement this and the problem is that in _get_related_lanesection() not value for the linktype is calculated for the case of junction connecting roads:

https://github.com/pyoscx/scenariogeneration/blob/2320db95e1ad0e826e661789ff65db70eeae0c5a/scenariogeneration/xodr/links.py#L913

johschmitz commented 1 year ago

I created a PR to hopefully fix this #176 We need another one though I believe because there also seems to be a bug for U turns in the _get_related_lanesection() method.

mander76 commented 1 year ago

Ok, to try to summarize, you want to connect two junctions directly, without a normal road between them?

johschmitz commented 1 year ago

@mander76 no that is not the case, I just want to create connecting roads for a single junction. But one case is a U-turn where a connecting road connects back the opposite side of the same incoming road.

mander76 commented 1 year ago

Aha, that use case I haven't thought about..

So technically you need a connecting road where the predecessor and successor is the same road..

mander76 commented 1 year ago

There is one problem I'm not quite sure about how to solve. In pic you show, there is no problem and a connecting road can easily be created between two lanes (like right side of the fig below), however for the case to the left no such lane can be created (it will have 0 length at the red cross), hence a offset connecting road has to be created instead..

Maybe the case to the left, always doing the offset could be a generic solution to this nicely..

image

johschmitz commented 1 year ago

I thought about this problem before multiple times and also while working on this. I think a good solution for junctions would be to move the reference line to the other side for the connecting road, so for the example above it could be on the right side of the connecting road and the connecting road lane would be the lane with ID 1 to avoid the issue. But this might violate the standard (I am not sure, maybe not if inside a junction..). Although in OpenDRIVE 1.8 this might work in a compliant way if we specify a separate driving direction which should now be possible as far as I remember. Maybe this can be discussed as part of OpenDRIVE 1.9 as well. I already thought about creating an ASAM Gitlab Issue for this. Otherwise an offset needs to be used as you mention to achieve the same result.

But for now on your side it would be enough to allow connecting the lanes and assume that there is a median and this does not happen as a first step.

image

johschmitz commented 1 year ago

Here is an example I build in esmini: image If you could merge #176 at least the straight links would work and I could make a release for that maybe if I manage before my holidays. Then you could think about the U-turn separately.

Note that here I used the trick to put the reference line towards the outside to avoid the U-turn geometry problems but I did not use lane offset for that.

mander76 commented 1 year ago

Ah cool, I'll setup a road today and test it out

mander76 commented 1 year ago

Hmm, is it actually needed? I can make a U-turn using the CommonJunctionCreator (as long as I don't do the special case mentioned above).

image

This is on main branch with the following simple example:

`from scenariogeneration import xodr, esmini import numpy as np

road1 = xodr.create_road(xodr.Arc(0.01,angle=np.pi3/4),0, left_lanes=2, right_lanes=2) road2 = xodr.create_road(xodr.Arc(0.01,angle=np.pi3/4),1, left_lanes=2, right_lanes=2,)

road1.add_successor(xodr.ElementType.road,1,xodr.ContactPoint.start) road2.add_predecessor(xodr.ElementType.road,0,xodr.ContactPoint.end)

odr = xodr.OpenDrive('my road') odr.add_road(road1) odr.add_road(road2)

junc = xodr.CommonJunctionCreator(10,'test')

junc.add_incoming_road_cartesian_geometry(road1,100/np.sqrt(2),0,-np.pi*3/4,'predecessor') junc.add_incoming_road_cartesian_geometry(road2,-100/np.sqrt(2),0,-np.pi/4,'successor') junc.add_connection(0,1) junc.add_connection(0,0,2,-2)

odr.add_junction_creator(junc)

odr.adjust_roads_and_lanes() esmini(odr,"/home/mander76/local/scenario_creation/esmini")`

johschmitz commented 1 year ago

Okay but what happens in the background?There should be an option for the user to use the low level API and make all the connections himself, don't you agree? Maybe I am missing something. Can you explain how your code does the lane level linking under the hood? Does it use internal APIs for that? Edit: I will also have a look if I can use this.

mander76 commented 1 year ago

Well, it's hard to explain almost 900 lines of python code, because there is no easy solution for junctions in OpenDRIVE....

However, you pointed out the example yourself how to make all Connections yourself (https://github.com/pyoscx/scenariogeneration/blob/main/examples/xodr/junction_trippel_twoway.py), which only uses public APIs. But you need to know what you are doing and know exactly how junctions work in OpenDRIVE to make use of it, including know how the connections are made and to have the correct signs for all links.

And to be frank, the JunctionCreators are made to actually make it "possible" to create junctions easily, because it is not easy, that functionality took me many weeks to "finish", to be able to handle all different cases. But it actually doesn't use much internal APIs, only two be exact, where _get_related_lanesection (which could be public APIs I would agree) is by far the most complicated one, and _create_junction_links is just a small for loop.

Probably the most important thing is to keep a very strict track of predecessor/successors. Almost all "if"s in the JunctionCreators includes successor/predecessor (and there are many "if"s due to this). These will give you info about contact_points, signs of connections, directions for offsets, and more...

I would suggest to go through the __create_connecting_roads_unequallanes or __create_connecting_road_with_laneinput in the CommonJunctionCreator. But in short:

  1. Figure out offsets needed (if it is not a connecting road that goes from reference line to reference line, but needed in this example we are talking about here)
  2. create the connecting_road with correct number of left/right lanes (will be different different depending on successor/predecessor)
  3. add predecessor and successor to the conneting_road, and correct offsets to the connecting_road aswell as the roads going into the junction
  4. Figure out the lanes to connect in the Junction (here the signs are the tricky thing usually)

As mentioned in our previous discussions, this package is firsthand made for scripting roads, but all "xml creating" classes are there and in the public API.

johschmitz commented 1 year ago

Well I think I am doing more or less just what you explain but you are using odr.adjust_roads_and_lanes() which I don't use and that is what creates the links for you. Instead (the end of) my code looks like this

    self.report({'ERROR'}, 'Only single lane connecting roads are supported!')
    connection = xodr.Connection(incoming_road.id, connecting_road.id, xodr.ContactPoint.start)
    connection.add_lanelink(lane_id_incoming, lane_id_connecting)
    junction.add_connection(connection)
    connection = xodr.Connection(outgoing_road.id, connecting_road.id, xodr.ContactPoint.end)
    connection.add_lanelink(lane_id_outgoing, lane_id_connecting)
    junction.add_connection(connection)

    xodr.create_lane_links_from_ids(incoming_road, connecting_road, [lane_id_incoming], [lane_id_connecting])
    xodr.create_lane_links_from_ids(connecting_road, outgoing_road, [lane_id_connecting], [lane_id_outgoing])

But the create_lane_links_from_ids() does not work for connecting roads without patching it.. (#176)

I can't use the adjust_roads_and_lanes() because it does more than just linking... I wonder though how it handles the U-turn case, I need to go through your code to understand that and I think the same thing needs to be done in create_lane_links_from_ids().

johschmitz commented 1 year ago

Okay the answer is that you are using an internal API:

    elif road1.road_type != -1:
        _create_links_connecting_road(road1, road2)
    elif road2.road_type != -1:
        _create_links_connecting_road(road2, road1)

see: https://github.com/pyoscx/scenariogeneration/blob/7c8e00ee62bf3044e39f1fc38dfd9cb3a885ca64/scenariogeneration/xodr/links.py#L755

So I need a public version of that API. Hm, maybe this internal API can be called for the same case in create_lane_links_from_ids(). I guess that would solve it. Need to update my PR then I think.

Edit: I am not sure if this is easily possible since _create_links_connecting_road() does not have an input for the lanes to be linked. We need something like _create_links_connecting_road_from_id() @mander76 what do you think?

Edit2: In fact your example from above is incompletely linked and esmini shows the error message:

No connection from rid 0 lid -1 -> rid 1 eltype 1 - trying move to closest lane
Connection found (rid 1 lid -1)

while running it.

mander76 commented 1 year ago

Hmm, this part of the code I haven't look at for awhile :P

But it looks like _create_lanelinks handles all type of connections in adjust_roads_and_lanes (and is public). The doc says "NOTE: now only works for roads/connecting roads with the same amount of lanes", but I almost think it's an old not correct to be honest, it looks like it takes into account some lane info further down in the code. However then the lane_offset_pred has to be filled correctly on the roads.

I think it's this function you should work with, and we can possibly extend if needed. Since it apparently handles U-turns already :)

johschmitz commented 1 year ago

The important thing is to handle roads with different amounts of lanes. This is the main point to consider. I don't think create_lane_links() can do that and that's why you introduced create_lane_links_from_ids() because I complained and now we also need this for connecting roads and U-turn connecting roads. I hope I remember the history correctly.

mander76 commented 1 year ago

It actually works, it's the "Note" that has to be removed, it can handle different amount of lanes (pushed already to main)

Here is an example (not using adjust_roads_and_lanes)

from scenariogeneration import xodr
import numpy as np
import os

roads = []

main_road = xodr.create_road(xodr.Line(100),0,2,2)
main_road.planview.set_start_point(0,0,0)
main_road.planview.adjust_geometries()

roads.append(main_road)

conn_road = xodr.create_road(xodr.Arc(1/3,angle=np.pi),id=1,left_lanes=0,road_type=1)
conn_road.planview.set_start_point(100,-3,0)
conn_road.planview.adjust_geometries()
roads.append(conn_road)

main_road.add_successor(xodr.ElementType.junction, 1)

conn_road.add_predecessor(xodr.ElementType.road, 0, xodr.ContactPoint.end,-1)
conn_road.add_successor(xodr.ElementType.road, 0, xodr.ContactPoint.end,1)

# create the opendrive
odr = xodr.OpenDrive("myroad")

odr.add_road(main_road)
odr.add_road(conn_road)

# # create junction
junction = xodr.Junction("test", 1)
con1 = xodr.Connection(0, 1, xodr.ContactPoint.start)
con1.add_lanelink(-2, -1)

junction.add_connection(con1)

odr.add_junction(junction)
# odr.adjust_roads_and_lanes()

xodr.create_lane_links(main_road, conn_road)

# write the OpenDRIVE file as xodr using current script name
# odr.write_xml(os.path.basename(__file__).replace(".py", ".xodr"))

# uncomment the following lines to display the road using esmini
from scenariogeneration import esmini
esmini(odr,os.path.join('/home/mander76/local/scenario_creation/esmini'))
johschmitz commented 1 year ago

Okay cool, interesting. Are you sure though that the result is already completely correct though? I looked at the lane links inside the roads and they seem incomplete? I remember @eknabe telling me in another esmini issue there should be complete link information in the junction as well as in the road link elements.

johschmitz commented 1 year ago

Alright maybe you need to add a line

xodr.create_lane_links(main_road, conn_road)
xodr.create_lane_links(conn_road, main_road)

and then it could be complete but this seems to not add two lane links in the road, maybe not necessary? I thought we need two but it seems to be working with esmini anyway. Nice, thank you! I tried it with my tool and still have to fix some bugs regarding the lane IDs on my end though it seems :D

I think this whole discussion shows again that it would be super useful if the esmini odrviewer could visualize the lane links...

johschmitz commented 1 year ago

I was trying with this example and using the xodr.create_lane_links() method but it just does not come up with the right lane IDs (the signs are inverted?):

image

bdsc_export.zip

Is this a bug? I think I will create a minimal example for you to make it easier to debug. Also is there a way to explicitly link the lanes I want to link?

johschmitz commented 1 year ago

Here is my minimal example with two incoming roads and one connecting road:

from scenariogeneration import xodr
import numpy as np
import os

roads = []

# Create junction
junction = xodr.Junction("junction_id_3", 3)

# Create roads
incoming_road = xodr.create_road(xodr.Line(100),id=0,left_lanes=1,right_lanes=0)
incoming_road.planview.set_start_point(-10.0,0.0,np.pi)
incoming_road.planview.adjust_geometries()
roads.append(incoming_road)

outgoing_road = xodr.create_road(xodr.Line(100),id=1,left_lanes=0,right_lanes=1)
outgoing_road.planview.set_start_point(0.0,10.0,np.pi/2)
outgoing_road.planview.adjust_geometries()
roads.append(outgoing_road)

connecting_road = xodr.create_road(xodr.Arc(1/13.0,angle=np.pi/2),id=2,left_lanes=1,right_lanes=0,road_type=3)
connecting_road.planview.set_start_point(-10.0,-3.0,0)
connecting_road.planview.adjust_geometries()
roads.append(connecting_road)

# Successors and predecessors on road level
incoming_road.add_predecessor(xodr.ElementType.junction, 3)
outgoing_road.add_predecessor(xodr.ElementType.junction, 3)

connecting_road.add_predecessor(xodr.ElementType.road, 0, xodr.ContactPoint.start,0)
connecting_road.add_successor(xodr.ElementType.road, 1, xodr.ContactPoint.start,0)

# Incoming junction connection
connection_1 = xodr.Connection(0, 2, xodr.ContactPoint.start)
connection_1.add_lanelink(1, 1)
junction.add_connection(connection_1)

# "Outgoing" junction connection
connection_2 = xodr.Connection(1, 2, xodr.ContactPoint.end)
connection_2.add_lanelink(1, -1)
junction.add_connection(connection_2)

# Create the road lane links
xodr.create_lane_links(incoming_road, connecting_road)
xodr.create_lane_links(connecting_road, outgoing_road)

# Create the OpenDRIVE
odr = xodr.OpenDrive("myroadnetwork")
odr.add_road(incoming_road)
odr.add_road(outgoing_road)
odr.add_road(connecting_road)
odr.add_junction(junction)

# Write the OpenDRIVE file as xodr using current script name
odr.write_xml(os.path.basename(__file__).replace(".py", ".xodr"))

# Display the road network using esmini's odrviewer tool
from scenariogeneration import esmini
esmini(odr,os.path.join('/opt/esmini'))

When you finde time can you maybe check while it is not working? Also I would still prefer to be able to explicitly link lanes instead of using this "offset" approach which always gives me trouble..

mander76 commented 1 year ago

Your example is wrong, you are not considering the driving directions of the roads correctly, the connecting road has the wrong lane in this case to be connected that way. And as you mentioned above, _create_lanelinks has to be used in both directions (not sure why though, that needs some looking into).

The problem is that your connecting road, connected like that needs a right lane, not a left lane, then adjusting some positions, aswell as the double _create_lanelinks

This is the correct example for your case

from scenariogeneration import xodr
import numpy as np
import os

roads = []

# Create junction
junction = xodr.Junction("junction_id_3", 3)

# Create roads
incoming_road = xodr.create_road(xodr.Line(100),id=0,left_lanes=1,right_lanes=0)
incoming_road.planview.set_start_point(-10.0,0.0,np.pi)
incoming_road.planview.adjust_geometries()
roads.append(incoming_road)

outgoing_road = xodr.create_road(xodr.Line(100),id=1,left_lanes=0,right_lanes=1)
outgoing_road.planview.set_start_point(3.0,13.0,np.pi/2)
outgoing_road.planview.adjust_geometries()
roads.append(outgoing_road)

connecting_road = xodr.create_road(xodr.Arc(1/13.0,angle=np.pi/2),id=2,left_lanes=0,right_lanes=1,road_type=3)
connecting_road.planview.set_start_point(-10.0,.0,0)
connecting_road.planview.adjust_geometries()
roads.append(connecting_road)

# Successors and predecessors on road level
incoming_road.add_predecessor(xodr.ElementType.junction, 3)
outgoing_road.add_predecessor(xodr.ElementType.junction, 3)

connecting_road.add_predecessor(xodr.ElementType.road, 0, xodr.ContactPoint.start,0)
connecting_road.add_successor(xodr.ElementType.road, 1, xodr.ContactPoint.start,0)

# Incoming junction connection
connection_1 = xodr.Connection(0, 2, xodr.ContactPoint.start)
connection_1.add_lanelink(1, -1)
junction.add_connection(connection_1)

# # "Outgoing" junction connection
# connection_2 = xodr.Connection(1, 2, xodr.ContactPoint.end)
# connection_2.add_lanelink(-1, 1)
# junction.add_connection(connection_2)

# Create the OpenDRIVE
odr = xodr.OpenDrive("myroadnetwork")
odr.add_road(incoming_road)
odr.add_road(outgoing_road)
odr.add_road(connecting_road)
odr.add_junction(junction)

# Create the road lane links
xodr.create_lane_links(incoming_road, connecting_road)
xodr.create_lane_links(connecting_road,incoming_road )
xodr.create_lane_links(connecting_road, outgoing_road)
xodr.create_lane_links(outgoing_road, connecting_road)

# Display the road network using esmini's odrviewer tool
from scenariogeneration import esmini
esmini(odr,os.path.join('/opt/esmini'))
johschmitz commented 1 year ago

It is correct that it is "wrong" if we do not consider the left hand side driving case but I would like to be able to build it that way to demonstrate that it would help solving the u-turn issue. In my opinion it should just be possible for the user to build such things using a low level API and exactly specifying the desired lane links, even if they are "wrong".

What I don't get it the part that you commented out. It results in

    <junction name="junction_id_3" id="3" type="default">
        <connection incomingRoad="0" id="0" contactPoint="start" connectingRoad="2">
            <laneLink from="1" to="-1"/>
        </connection>
    </junction>

and

    <road rule="RHT" id="2" junction="3" length="20.420352248333657">
        <link>
            <predecessor elementType="road" elementId="0" contactPoint="start"/>
            <successor elementType="road" elementId="1" contactPoint="start"/>
        </link>
[...]
                </center>
                <right>
                    <lane id="-1" type="driving" level="false">
                        <link>
                            <predecessor id="1"/>
                            <successor id="-1"/>
                        </link>
[...]
                    </lane>
                </right>
            </laneSection>
        </lanes>
    </road>

To the best of my knowledge that is incomplete. See Section10.2. Incoming roads 10.2. Incoming roads in OpenDRIVE 1.7: "Incoming roads contain lanes that lead into a junction. Because outgoing roads are not specifically defined in ASAM OpenDRIVE, incoming roads may also serve as outgoing roads, see Figure 77." I think we need both links in the junction, i.e. the junction should have two connection elements. Maybe I do not understand the connection.add_lanelink() method correctly, are those the lanes of the incoming and the connecting road or both are for incoming roads? The documentation of that method does not seem to be in line with the OpenDRIVE terminology. Edit: Let me explain this better, connection = xodr.Connection() creates a link between an incoming and a connecting road but connection.add_lanelink() does not create a lane link between the incoming and the connecting road as it seems which is very confusing. Maybe I should build the same minmal example using xodr.create_lane_links_from_ids() to explain how I wanted to do it.

johschmitz commented 1 year ago

So here is the alternative example which produces all the lane links in the road as well as the junction connections. But this only works if you apply PR #176 :

from scenariogeneration import xodr
import numpy as np
import os

roads = []

# Create junction
junction = xodr.Junction("junction_id_3", 3)

# Create roads
incoming_road = xodr.create_road(xodr.Line(100),id=0,left_lanes=3,right_lanes=0)
incoming_road.planview.set_start_point(0.0,0.0,np.pi)
incoming_road.planview.adjust_geometries()
roads.append(incoming_road)

outgoing_road = xodr.create_road(xodr.Line(100),id=1,left_lanes=0,right_lanes=3)
outgoing_road.planview.set_start_point(7.0,7.0,np.pi/2)
outgoing_road.planview.adjust_geometries()
roads.append(outgoing_road)

connecting_road = xodr.create_road(xodr.Arc(1/13.0,angle=np.pi/2),id=2,left_lanes=0,right_lanes=1,road_type=3)
connecting_road.planview.set_start_point(0.0,-6.0,.0)
connecting_road.planview.adjust_geometries()
roads.append(connecting_road)

# Successors and predecessors on road level
incoming_road.add_predecessor(xodr.ElementType.junction, 3)
outgoing_road.add_predecessor(xodr.ElementType.junction, 3)

connecting_road.add_predecessor(xodr.ElementType.road, 0, xodr.ContactPoint.start,0)
connecting_road.add_successor(xodr.ElementType.road, 1, xodr.ContactPoint.start,0)

# Incoming junction connection
connection_1 = xodr.Connection(0, 2, xodr.ContactPoint.start)
connection_1.add_lanelink(3, -1)
junction.add_connection(connection_1)

# "Outgoing" junction connection
connection_2 = xodr.Connection(1, 2, xodr.ContactPoint.end)
connection_2.add_lanelink(-1, -3)
junction.add_connection(connection_2)

# Create the road lane links
xodr.create_lane_links_from_ids(incoming_road, connecting_road, [3], [-1])
xodr.create_lane_links_from_ids(connecting_road, outgoing_road, [-1], [-3])

# Create the OpenDRIVE
odr = xodr.OpenDrive("myroadnetwork")
odr.add_road(incoming_road)
odr.add_road(outgoing_road)
odr.add_road(connecting_road)
odr.add_junction(junction)

# Write the OpenDRIVE file as xodr using current script name
odr.write_xml(os.path.basename(__file__).replace(".py", ".xodr"))

# Display the road network using esmini's odrviewer tool
from scenariogeneration import esmini
esmini(odr,os.path.join('/opt/esmini'))

image

mander76 commented 1 year ago

Well, maybe I'm wrong about the exit road, but that's an easy fix.

Lefthand driving is one thing, and that is fine. However mixing left hand right hand driving in a junction is not a good idea (like your first example), that I can directly say that scenariogeneration is not supporting, it's hard as it is to handle junctions without mixing driving directions. And if you want to create a correct OpenDRIVE as it looks like you want, I really don't get why you wanna switch driving directions?

As for creating whatever the user wants, the xodr module is to nice now that you can create whatever, but it is a big job to add exceptions to all possible wrong ways of doing it (which I would like to but have higher prio stuff I would like to do if I get that much time).

johschmitz commented 1 year ago

Okay let's forget about the "wrong" road side issue and focus on the "correct" way of building it.

For that case could you consider my example above which fixes the problems in your example? Or maybe explain why they are no problems?

Edit: maybe to make it very clear, my goal is to make it possible to build something like the example from the standard document 1.7, section 10.3: image

Edit2: In the tool it would look like this (just created some subset of the connecting roads to avoid confusion, also assuming right hand side driving): image

Also if possible please let me know what is blocking the PR, I am willing to spend time on improving/fixing it if necessary.

mander76 commented 1 year ago

Well, I don't see a problem creating those kinds of junctions already, it's done in the CommonJunctionCreator already? Might be that some API needs to go public but more than that I don't see a limitation? But I will double check with your last example (have a rough week).

And since I don't know what the PR is supposed to fix, it's quite hard to see why it needs merging.

mander76 commented 1 year ago

As for your last example, you can just use create_lane_links again, with just adding the correct offsets to the add_predecessor and add_successor method calls for the connecting road

from scenariogeneration import xodr
import numpy as np
import os

roads = []

# Create junction
junction = xodr.Junction("junction_id_3", 3)

# Create roads
incoming_road = xodr.create_road(xodr.Line(100),id=0,left_lanes=3,right_lanes=0)
incoming_road.planview.set_start_point(0.0,0.0,np.pi)
incoming_road.planview.adjust_geometries()
roads.append(incoming_road)

outgoing_road = xodr.create_road(xodr.Line(100),id=1,left_lanes=0,right_lanes=3)
outgoing_road.planview.set_start_point(7.0,7.0,np.pi/2)
outgoing_road.planview.adjust_geometries()
roads.append(outgoing_road)

connecting_road = xodr.create_road(xodr.Arc(1/13.0,angle=np.pi/2),id=2,left_lanes=0,right_lanes=1,road_type=3)
connecting_road.planview.set_start_point(0.0,-6.0,.0)
connecting_road.planview.adjust_geometries()
roads.append(connecting_road)

# Successors and predecessors on road level
incoming_road.add_predecessor(xodr.ElementType.junction, 3)
outgoing_road.add_predecessor(xodr.ElementType.junction, 3)

connecting_road.add_predecessor(xodr.ElementType.road, 0, xodr.ContactPoint.start,2) # change offset here
connecting_road.add_successor(xodr.ElementType.road, 1, xodr.ContactPoint.start,-2) # change offset here

# Incoming junction connection
connection_1 = xodr.Connection(0, 2, xodr.ContactPoint.start)
connection_1.add_lanelink(3, -1)
junction.add_connection(connection_1)

# "Outgoing" junction connection
connection_2 = xodr.Connection(1, 2, xodr.ContactPoint.end)
connection_2.add_lanelink(-1, -3)
junction.add_connection(connection_2)

# Create the road lane links
# xodr.create_lane_links_from_ids(incoming_road, connecting_road, [3], [-1])
# xodr.create_lane_links_from_ids(connecting_road, outgoing_road, [-1], [-3])
xodr.create_lane_links(incoming_road, connecting_road) # change here
xodr.create_lane_links(connecting_road, outgoing_road) # change here
# Create the OpenDRIVE
odr = xodr.OpenDrive("myroadnetwork")
odr.add_road(incoming_road)
odr.add_road(outgoing_road)
odr.add_road(connecting_road)
odr.add_junction(junction)

# Write the OpenDRIVE file as xodr using current script name
odr.write_xml(os.path.basename(__file__).replace(".py", ".xodr"))

# Display the road network using esmini's odrviewer tool
from scenariogeneration import esmini
esmini(odr,os.path.join('/opt/esmini'))
johschmitz commented 1 year ago

I will check this example out later, thx.

Regarding the PR, clearly the create_lane_links_from_ids() method does not support junction connecting roads and I wanted to fix that. What is bad about that? For a general motivation if you need/want that, the idea is to be able to explicitly perform the lane level linking instead of implicitly through the offset on the road level. There seems to also be no other public API to do that. From what I understand the implicit way with the offset also does not work for arbitrary number of lanes which are not the same on both sides or was that problem solved in the meantime?

johschmitz commented 1 year ago

I tested your example and extended it to a second connecting road on the other road side and it does work. I also created a PR to fix the wrong documentation of the add_lanelink() method: #181

If possible I would still like to fix the create_lane_links_from_ids() as well if the other PR #176 does not break anything else but I think I can now survive. Will try.

johschmitz commented 1 year ago

@mander76 Although it runs with esmini, and I also got it now running in my export :partying_face: that way, I noticed again that with your approach the lane link information in the incoming roads is missing. I think it is not necessary because it is available in the junction but I am unsure if this is standard compliant. Some tools might expect this information. What do you think?

Overall my feedback though (still) is that the API with the lane offset is non intuitive because you end up writing code like this in order to calculate it:

lanel_offset_incoming = copysign(abs(lane_id_incoming) - 1, lane_id_incoming)
mander76 commented 1 year ago

I agree that lane_offset could be done in another way, however a large refactoring would be necessary in order to make it work for the whole package.

Personally I would prefer to remove the create_lane_links_from_ids() API in general since apparently it works already with the "standard API". However I see the use case for it since it might make life easier for your case, and the lane offset is not the best solution. So we can try to fix it in that case, however now it looks like it adds multiple links, which I don't agree with

As far as I understand from the user guide and examples, the links are created correctly as it is now.

From User Guide:

An incoming road with multiple lanes may be connected to the lanes of the road leading out off the junction in different ways:

By multiple connecting roads, each with one <laneLink> element for the connection between two specific lanes. Lane changes within this junction are not possible.

By one connecting road with multiple <laneLink> elements for the connections between the lanes. Lane changes within this junction are possible.

As well as the examples provided in the standard (eg. UC_Simple-X-Junction.xodr)

johschmitz commented 1 year ago

I am not exactly sure what you are referring to regarding the links, could you provide a bit more context?

What I was referring to is that the incoming road lane element should not have empty link elements. What do you think about that part? Do you believe the incoming roads should have lane link information or not? See also this answer for reference: https://github.com/esmini/esmini/issues/328#issuecomment-1276183592

mander76 commented 1 year ago

That was my reply: No, they should not have them according to the examples and the text in the standard.

johschmitz commented 1 year ago

I am fine with that but I do not see that the standard document clearly defines it. But then lets just follow the examples from the standard supplements as you suggested. Also I hope you are aligned with @eknabe on this issue.

mander76 commented 1 year ago

We should be, and looking again in the Link section of lanes (9.4)

Rules

The following rules apply to lane linkage:

A lane may have another lane as predecessor or successor.

Two lanes shall only be linked if their linkage is clear. If the relationship to a predecessor or successor is ambiguous, junctions shall be used.

Multiple predecessors and successors shall be used if a lane is split abruptly or several lanes are merged abruptly. All lanes that are connected shall have a non-zero width at the connection point.

Lanes that have a width of zero at the beginning of the lane section shall have no <predecessor> element.

Lanes that have a width of zero at the end of the lane section shall have no <successor> element.

The <link> element shall be omitted if the lane starts or ends in a junction or has no link.

Especiallly the last one The element shall be omitted if the lane starts or ends in a junction or has no link.

johschmitz commented 1 year ago

I also had a discussion about this in the standardization meeting and it turns out you are right that there is usually no lane link information in roads at the connection point connected to junctions. What I was also remembered about and what I find a bit shocking is that in the junction there should be no connection elements for "outgoing" lane links. So if you want to find out coming from the "outgoing" lane what connecting road lane connects to it, you need to first find all connecting roads for that junction among all roads in the map and then look in there for the lane link. I really don't like this design but that is apparently the idea how to interpret the current standard..