Esempio n. 1
0
    def __init__(self,
                 roadBuilder=None,
                 straightRoadLen=10,
                 minAngle=np.pi / 6,
                 maxAngle=None,
                 country=CountryCodes.US,
                 random_seed=39):

        self.config = Configuration()
        self.roadBuilder = roadBuilder

        if self.roadBuilder is None:
            self.roadBuilder = RoadBuilder()

        self.straightRoadBuilder = StraightRoadBuilder()
        self.laneBuilder = LaneBuilder()

        self.straightRoadLen = straightRoadLen

        self.minAngle = minAngle
        self.maxAngle = maxAngle

        if self.maxAngle == np.pi:
            self.maxAngle = 0.99 * np.pi

        self.laneWidth = self.config.get("default_lane_width")
        self.minConnectionLength = self.config.get("min_connection_length")
        self.maxConnectionLength = self.config.get("max_connection_length")
        self.curveBuilder = CurveRoadBuilder(country=country)
        self.connectionBuilder = ConnectionBuilder()
        self.countryCode = country
        np.random.seed(random_seed)
        pass
Esempio n. 2
0
    def setUp(self):

        self.configuration = Configuration()
        self.esminiPath = self.configuration.get("esminipath")
        self.configuration = Configuration()

        self.roadBuilder = RoadBuilder()
        self.junctionBuilder = JunctionBuilder()
        outputDir = os.path.join(os.getcwd(), 'output')
        lastId = 0
        self.harvester = JunctionHarvester(outputDir=outputDir,
                                           outputPrefix='test_',
                                           lastId=lastId,
                                           minAngle=np.pi / 30,
                                           maxAngle=np.pi)
Esempio n. 3
0
    def __init__(self, 
                outputDir, 
                outputPrefix, 
                lastId=0, 
                minAngle = np.pi / 10, 
                maxAngle = np.pi, 
                straightRoadLen = 10,
                esminiPath = None, 
                saveImage = True):
        """The angle between two connected roads are >= self.minAngle <= self.maxAngle

        Args:
            outputDir ([type]): [description]
            outputPrefix ([type]): [description]
            lastId (int, optional): [description]. Defaults to 0.
            minAngle ([type], optional): [description]. Defaults to np.pi/10.
            maxAngle ([type], optional): [description]. Defaults to np.pi.
            straightRoadLen : used both for normal and connection roads
            esminiPath: Path to esmini to generate images for roads.
        """

        self.destinationPrefix = os.path.join(outputDir, outputPrefix)
        self.minAngle = minAngle
        self.maxAngle = maxAngle
        self.lastId = lastId
        self.straightRoadLen = straightRoadLen

        self.roadBuilder = RoadBuilder()
        self.straightRoadBuilder = StraightRoadBuilder()

        self.junctionBuilder = JunctionBuilder(self.roadBuilder)
        self.sequentialJunctionBuilder = SequentialJunctionBuilder(self.roadBuilder, self.straightRoadLen, minAngle=minAngle, maxAngle=maxAngle)

        self.junctionMerger = JunctionMerger(outputDir, outputPrefix, lastId)

        self.configuration = Configuration()

        if esminiPath is None:
            self.esminiPath = self.configuration.get("esminipath")
        else:
            self.esminiPath = esminiPath

        self.saveImage = saveImage

        if os.path.isdir(self.esminiPath) is False:
            logging.warn(f"Esmini path not found {self.esminiPath}. Will break if you try to save images using harvester.")

        pass
Esempio n. 4
0
    def __init__(self,
                 outputDir,
                 outputPrefix='R3_',
                 lastId=0,
                 esminiPath=None,
                 saveImage=True):
        """

        Args:
            outputDir ([type]): [description]
            outputPrefix ([type]): [description]
            lastId (int, optional): [description]. Defaults to 0.
        """

        self.destinationPrefix = os.path.join(outputDir, outputPrefix)
        self.lastId = lastId

        self.roadBuilder = RoadBuilder()

        self.configuration = Configuration()

        if esminiPath is None:
            self.esminiPath = self.configuration.get("esminipath")
        else:
            self.esminiPath = esminiPath

        self.saveImage = saveImage

        if os.path.isdir(self.esminiPath) is False:
            logging.warn(
                f"Esmini path not found {self.esminiPath}. Will break if you try to save images using merger."
            )

        pass
    def setUp(self):

        self.configuration = Configuration()
        self.esminiPath = self.configuration.get("esminipath")
        self.roadBuilder = RoadBuilder()
        self.laneBuilder = LaneBuilder()
        self.laneLinker = LaneLinker()
        self.straightRoadBuilder = StraightRoadBuilder()
Esempio n. 6
0
    def __init__(self, outputDir, outputPrefix='R3_', lastId=0):
        """

        Args:
            outputDir ([type]): [description]
            outputPrefix ([type]): [description]
            lastId (int, optional): [description]. Defaults to 0.
        """

        self.destinationPrefix = os.path.join(outputDir, outputPrefix)
        self.lastId = lastId

        self.roadBuilder = RoadBuilder()

        pass
Esempio n. 7
0
class test_RoadBuilder(unittest.TestCase):
    def setUp(self):

        self.configuration = Configuration()
        self.esminiPath = self.configuration.get("esminipath")
        self.configuration = Configuration()

        self.roadBuilder = RoadBuilder()
        self.junctionBuilder = JunctionBuilder()
        outputDir = os.path.join(os.getcwd(), 'output')
        lastId = 0
        self.harvester = JunctionHarvester(outputDir=outputDir,
                                           outputPrefix='test_',
                                           lastId=lastId,
                                           minAngle=np.pi / 30,
                                           maxAngle=np.pi)

    def test_ParamPoly(self):
        tangentX = np.array([9.389829642616592, -7.596531772501544])
        tangentY = np.array([0.0, 5.5192033616035365])

        t = np.array([0, 1])
        x = np.array([0, 17.8605173461395])
        y = np.array([0, -5.803233839653106])
        hermiteX = CubicHermiteSpline(t, x, tangentX)

        hermiteY = CubicHermiteSpline(t, y, tangentY)
        xCoeffs = hermiteX.c.flatten()
        yCoeffs = hermiteY.c.flatten()

        # scipy coefficient and open drive coefficents have opposite order.
        myRoad = self.roadBuilder.curveBuilder.createParamPoly3(
            0,
            isJunction=False,
            au=xCoeffs[3],
            bu=xCoeffs[2],
            cu=xCoeffs[1],
            du=xCoeffs[0],
            av=yCoeffs[3],
            bv=yCoeffs[2],
            cv=yCoeffs[1],
            dv=yCoeffs[0])

        odr = pyodrx.OpenDrive("test")
        odr.add_road(myRoad)
        odr.adjust_roads_and_lanes()

        extensions.printRoadPositions(odr)

        extensions.view_road(
            odr, os.path.join('..', self.configuration.get("esminipath")))

    def test_getConnectionRoadBetween(self):
        # test scenario for connection road

        roads = []
        roads.append(pyodrx.create_straight_road(0, 10))
        roads.append(
            self.roadBuilder.curveBuilder.createSimple(1,
                                                       np.pi / 4,
                                                       True,
                                                       curvature=0.2))
        roads.append(pyodrx.create_straight_road(2, 10))
        roads.append(
            self.roadBuilder.curveBuilder.createSimple(3,
                                                       np.pi / 3,
                                                       True,
                                                       curvature=0.2))
        roads.append(pyodrx.create_straight_road(4, 10))
        roads.append(
            self.roadBuilder.curveBuilder.createSimple(5,
                                                       np.pi / 2,
                                                       True,
                                                       curvature=0.2))
        roads.append(pyodrx.create_straight_road(6, 10))

        roads[0].add_successor(pyodrx.ElementType.junction, 1)

        # roads[1].add_predecessor(pyodrx.ElementType.road,0,pyodrx.ContactPoint.end)
        roads[1].add_predecessor(pyodrx.ElementType.road, 0,
                                 pyodrx.ContactPoint.start)
        roads[1].add_successor(pyodrx.ElementType.road, 2,
                               pyodrx.ContactPoint.start)

        roads[2].add_predecessor(pyodrx.ElementType.junction, 1)
        roads[2].add_successor(pyodrx.ElementType.junction, 3)

        roads[3].add_predecessor(pyodrx.ElementType.road, 2,
                                 pyodrx.ContactPoint.start)
        roads[3].add_successor(pyodrx.ElementType.road, 4,
                               pyodrx.ContactPoint.start)

        roads[4].add_predecessor(pyodrx.ElementType.junction, 3)
        roads[4].add_successor(pyodrx.ElementType.junction, 5)

        roads[5].add_predecessor(pyodrx.ElementType.road, 4,
                                 pyodrx.ContactPoint.start)
        roads[5].add_successor(pyodrx.ElementType.road, 6,
                               pyodrx.ContactPoint.start)

        roads[6].add_predecessor(pyodrx.ElementType.junction, 5)

        junction = self.junctionBuilder.createJunctionForASeriesOfRoads(roads)

        odrName = "test_connectionRoad"
        odr = extensions.createOdr(odrName, roads, [junction])

        lastConnection = self.harvester.junctionBuilder.createLastConnectionForLastAndFirstRoad(
            7, roads, junction, cp1=pyodrx.ContactPoint.start)
        roads.append(lastConnection)
        odr.add_road(lastConnection)

        # randConnection = self.harvester.junctionBuilder.createConnectionFor2Roads(8, roads[0], roads[4], junction, cp1=pyodrx.ContactPoint.start)
        # roads.append(randConnection)
        # odr.add_road(randConnection)

        # randConnection2 = self.harvester.junctionBuilder.createConnectionFor2Roads(9, roads[2], roads[6], junction, cp1=pyodrx.ContactPoint.start)
        # roads.append(randConnection2)
        # odr.add_road(randConnection2)

        # odr.reset()
        # odr.add_road(lastConnection)
        # odr.adjust_roads_and_lanes()

        odr.resetAndReadjust()

        # pyodrx.prettyprint(odr.get_element())

        odr.write_xml(f"output/test_connectionRoad.xodr")

        # extensions.printRoadPositions(odr)

        extensions.view_road(
            odr, os.path.join('..', self.configuration.get("esminipath")))
        pass

    def test_createMShape(self):

        roads = []
        roads.append(pyodrx.create_straight_road(0, 10))
        roads.append(self.roadBuilder.createMShape(1, 1, np.pi / 1.5, 10))
        roads.append(pyodrx.create_straight_road(2, 10))

        roads[0].add_successor(pyodrx.ElementType.junction, 1)

        roads[1].add_predecessor(pyodrx.ElementType.road, 0,
                                 pyodrx.ContactPoint.end)
        # roads[1].add_predecessor(pyodrx.ElementType.road,0,pyodrx.ContactPoint.start)
        roads[1].add_successor(pyodrx.ElementType.road, 2,
                               pyodrx.ContactPoint.start)

        roads[2].add_predecessor(pyodrx.ElementType.junction, 1)

        junction = self.junctionBuilder.createJunctionForASeriesOfRoads(roads)

        odrName = "test_connectionRoad"
        odr = extensions.createOdr(odrName, roads, [junction])

        extensions.view_road(
            odr, os.path.join('..', self.configuration.get("esminipath")))

        xmlPath = f"output/m-shape.xodr"
        odr.write_xml(xmlPath)

        extensions.saveRoadImageFromFile(xmlPath, self.esminiPath)

    def test_createMShapeLeftLanes(self):

        roads = []
        roads.append(pyodrx.create_straight_road(0, 10))
        roads.append(
            self.roadBuilder.createMShape(1,
                                          1,
                                          np.pi / 1.5,
                                          10,
                                          laneSides=junctions.LaneSides.LEFT))
        roads.append(pyodrx.create_straight_road(2, 10))

        roads[0].add_successor(pyodrx.ElementType.junction, 1)

        roads[1].add_predecessor(pyodrx.ElementType.road, 0,
                                 pyodrx.ContactPoint.end)
        # roads[1].add_predecessor(pyodrx.ElementType.road,0,pyodrx.ContactPoint.start)
        roads[1].add_successor(pyodrx.ElementType.road, 2,
                               pyodrx.ContactPoint.start)

        roads[2].add_predecessor(pyodrx.ElementType.junction, 1)

        junction = self.junctionBuilder.createJunctionForASeriesOfRoads(roads)

        odrName = "test_connectionRoad"
        odr = extensions.createOdr(odrName, roads, [junction])

        extensions.view_road(
            odr, os.path.join('..', self.configuration.get("esminipath")))

    def test_createMShapeRightLanes(self):

        roads = []
        roads.append(pyodrx.create_straight_road(0, 10))
        roads.append(
            self.roadBuilder.createMShape(
                1,
                1,
                -np.pi / 1.5,
                10,
                laneSides=junctions.LaneSides.RIGHT,
                direction=CircularDirection.COUNTERCLOCK_WISE))
        roads.append(pyodrx.create_straight_road(2, 10))

        roads[0].add_successor(pyodrx.ElementType.junction, 1)

        roads[1].add_predecessor(pyodrx.ElementType.road, 0,
                                 pyodrx.ContactPoint.end)
        # roads[1].add_predecessor(pyodrx.ElementType.road,0,pyodrx.ContactPoint.start)
        roads[1].add_successor(pyodrx.ElementType.road, 2,
                               pyodrx.ContactPoint.start)

        roads[2].add_predecessor(pyodrx.ElementType.junction, 1)

        junction = self.junctionBuilder.createJunctionForASeriesOfRoads(roads)

        odrName = "test_connectionRoad"
        odr = extensions.createOdr(odrName, roads, [junction])

        extensions.view_road(
            odr, os.path.join('..', self.configuration.get("esminipath")))
Esempio n. 8
0
 def setUp(self):
     self.roadBuilder = RoadBuilder()
     self.junctionBuilder = JunctionBuilder()
     self.configuration = Configuration()
     self.angleCurvatureMap = AngleCurvatureMap()
     self.esminiPath = self.configuration.get("esminipath")
Esempio n. 9
0
class JunctionBuilder:
    def __init__(self,
                 roadBuilder=None,
                 straightRoadLen=10,
                 minAngle=np.pi / 6,
                 maxAngle=None,
                 country=CountryCodes.US,
                 random_seed=39):

        self.config = Configuration()
        self.roadBuilder = roadBuilder

        if self.roadBuilder is None:
            self.roadBuilder = RoadBuilder()

        self.straightRoadBuilder = StraightRoadBuilder()
        self.laneBuilder = LaneBuilder()

        self.straightRoadLen = straightRoadLen

        self.minAngle = minAngle
        self.maxAngle = maxAngle

        if self.maxAngle == np.pi:
            self.maxAngle = 0.99 * np.pi

        self.laneWidth = self.config.get("default_lane_width")
        self.minConnectionLength = self.config.get("min_connection_length")
        self.maxConnectionLength = self.config.get("max_connection_length")
        self.curveBuilder = CurveRoadBuilder(country=country)
        self.connectionBuilder = ConnectionBuilder()
        self.countryCode = country
        np.random.seed(random_seed)
        pass

    def createJunctionForASeriesOfRoads(self, roads, id=0):
        """[summary]

        Args:
            roads ([type]): even indices are roads, odd indices are connection roads  of the junction

        Returns:
            [type]: [description]
        """

        # TODO it does not support all lanes.
        # ID is wrong
        junction = pyodrx.Junction("spiderJunction", id)

        connectionId = 1

        while (connectionId < len(roads)):

            print(f"connecting roads {connectionId-1} {connectionId}")
            connectionL = pyodrx.Connection(connectionId - 1, connectionId,
                                            pyodrx.ContactPoint.start)
            connectionL.add_lanelink(-1, -1)

            # if (connectionId + 1) < len(roads):
            #     connectionR = pyodrx.Connection(connectionId+1, connectionId, pyodrx.ContactPoint.end)
            # else:
            #     connectionR = pyodrx.Connection(0, connectionId, pyodrx.ContactPoint.end)

            # connectionR.add_lanelink(1,1)

            junction.add_connection(connectionL)
            # junction.add_connection(connectionR)

            connectionId += 2

        return junction

    def createLastConnectionForLastAndFirstRoad(self,
                                                nextRoadId,
                                                roads,
                                                junction,
                                                cp1=pyodrx.ContactPoint.end,
                                                cp2=pyodrx.ContactPoint.start):

        lastConnectionId = nextRoadId
        lastConnection = self.roadBuilder.getConnectionRoadBetween(
            lastConnectionId, roads[-1], roads[0], cp2, cp1)

        RoadLinker.createExtendedPredSuc(predRoad=roads[-1],
                                         predCp=cp2,
                                         sucRoad=lastConnection,
                                         sucCP=pyodrx.ContactPoint.start)
        RoadLinker.createExtendedPredSuc(predRoad=lastConnection,
                                         predCp=pyodrx.ContactPoint.end,
                                         sucRoad=roads[0],
                                         sucCP=cp1)

        connectionL = pyodrx.Connection(roads[-1].id, lastConnectionId,
                                        pyodrx.ContactPoint.start)
        connectionL.add_lanelink(-1, -1)
        junction.add_connection(connectionL)

        # roads.append(lastConnection) # dangerous. do not add the road

        return lastConnection

    def createConnectionFor2Roads(self,
                                  nextRoadId,
                                  road1,
                                  road2,
                                  junction,
                                  cp1,
                                  cp2,
                                  n_lanes=1,
                                  lane_offset=3,
                                  laneSides=LaneSides.BOTH):
        """Does not modify predecessor or successor of the given roads.

        Args:
            junction: the junction object to add links.

        Returns:
            [type]: connection road with first road as the predecessor and second road as the successor
        """

        newConnectionId = nextRoadId
        newConnectionRoad = self.roadBuilder.getConnectionRoadBetween(
            newConnectionId,
            road1,
            road2,
            cp1,
            cp2,
            isJunction=True,
            n_lanes=n_lanes,
            lane_offset=lane_offset,
            laneSides=laneSides)

        RoadLinker.createExtendedPredSuc(predRoad=road1,
                                         predCp=cp1,
                                         sucRoad=newConnectionRoad,
                                         sucCP=pyodrx.ContactPoint.start)
        RoadLinker.createExtendedPredSuc(predRoad=newConnectionRoad,
                                         predCp=pyodrx.ContactPoint.end,
                                         sucRoad=road2,
                                         sucCP=cp2)

        if junction is not None:
            if laneSides == LaneSides.LEFT or laneSides == LaneSides.BOTH:
                connectionL = pyodrx.Connection(road2.id, newConnectionId,
                                                pyodrx.ContactPoint.end)
                connectionL.add_lanelink(-1, -1)
                junction.add_connection(connectionL)
            else:
                connectionL = pyodrx.Connection(road1.id, newConnectionId,
                                                pyodrx.ContactPoint.start)
                connectionL.add_lanelink(1, 1)
                junction.add_connection(connectionL)

        return newConnectionRoad

    def createInternalConnectionsForConnectionSeres(self, roads,
                                                    connectionSeres, junction):
        """Assumes last road has the largest id. Used to create internal connections inside a junction. Assumes a connection series has middle roads which are connected by an internal connection road. Normally each series will have 3 roads.

        Args:
            roads ([type]): [description]
            connectionSeres ([type]): list of ConnectionSeries type
        """

        # for each last road in a series, connect with the next first road
        length = len(connectionSeres)
        nextRoadId = connectionSeres[-1].getLast().id + 1  # last id so far.

        for i in range(length):
            currentConnectionS = connectionSeres[i]

            if (i + 1) < length:
                nextConnectionS = connectionSeres[i + 1]
            else:
                nextConnectionS = connectionSeres[0]

            # traffic will go from current to next

            fromRoad = currentConnectionS.getMiddle()
            toRoad = nextConnectionS.getMiddle()

            print(
                f"creating internal connection from {fromRoad.id} to {toRoad.id}"
            )

            newConnection = self.createConnectionFor2Roads(
                nextRoadId,
                fromRoad,
                toRoad,
                cp1=pyodrx.ContactPoint.end,
                cp2=pyodrx.ContactPoint.start,
                junction=junction,
                laneSides=LaneSides.RIGHT)

            roads.append(newConnection)

            nextRoadId += 1

        return nextRoadId

    def buildSimpleRoundAbout(self,
                              odrId=0,
                              numRoads=4,
                              radius=10,
                              cp1=pyodrx.ContactPoint.start,
                              direction=CircularDirection.COUNTERCLOCK_WISE):
        """In a simple roundabout, there is a circle inside the junction, the connection roads reside in the circle.

        Args:
            numRoads (int, optional): [description]. Defaults to 4.
            radius : in meters.
            cp1: contact point on the first road.
        """

        anglePerRoad = (np.pi * 2) / numRoads

        roads = []
        roads.append(
            self.straightRoadBuilder.create(0, length=self.straightRoadLen))
        nextRoadId = 1

        roadsCreated = 1

        connectionSeres = [
        ]  # holds all the connection road series so that we can create internal connections later.

        while roadsCreated < numRoads:
            previousRoadId = nextRoadId - 1
            newConnectionId = nextRoadId

            # 1. create a new connection road series and increase nextRoadId
            newConnectionSeries = self.roadBuilder.createRoundAboutConnection(
                newConnectionId, anglePerRoad, radius)
            connectionSeres.append(newConnectionSeries)

            nextRoadId += newConnectionSeries.length()
            newRoadId = nextRoadId
            nextRoadId += 1

            # 2. create a road
            newRoad = self.straightRoadBuilder.create(
                newRoadId, length=self.straightRoadLen)

            # 3 add new roads
            roads += newConnectionSeries.getAll()
            roads.append(newRoad)

            roads[previousRoadId].addExtendedSuccessor(
                newConnectionSeries.getFirst(), 0, pyodrx.ContactPoint.start)

            if newConnectionSeries.getFirst().id == 1:
                newConnectionSeries.getFirst().addExtendedPredecessor(
                    roads[previousRoadId], 0, cp1)
                # TODO this is a hack. It will not eventually work because outgoing roads' ends will come to join other junctions.
            else:
                newConnectionSeries.getFirst().addExtendedPredecessor(
                    roads[previousRoadId], 0, pyodrx.ContactPoint.start)

            RoadLinker.createExtendedPredSuc(
                predRoad=newConnectionSeries.getLast(),
                predCp=pyodrx.ContactPoint.end,
                sucRoad=newRoad,
                sucCP=pyodrx.ContactPoint.start)

            # 6 get next action
            roadsCreated += 1

            pass

        lastRoad = roads[-1]
        # 3. create connections and junction

        junction = self.createJunctionForASeriesOfRoads(roads)

        # print(f"number of roads created {len(roads)}")
        odrName = 'Simple-Roundabout-' + str(numRoads) + '_L2_' + str(odrId)
        odr = extensions.createOdrByPredecessor(odrName, roads, [junction])

        # The last connection and resetting odr

        finalConnectionSeries = self.roadBuilder.createRoundAboutConnection(
            nextRoadId, anglePerRoad, radius)
        connectionSeres.append(finalConnectionSeries)

        roads += finalConnectionSeries.getAll()

        RoadLinker.createExtendedPredSuc(
            predRoad=lastRoad,
            predCp=pyodrx.ContactPoint.start,
            sucRoad=finalConnectionSeries.getFirst(),
            sucCP=pyodrx.ContactPoint.start)
        RoadLinker.createExtendedPredSuc(
            predRoad=finalConnectionSeries.getLast(),
            predCp=pyodrx.ContactPoint.end,
            sucRoad=roads[0],
            sucCP=cp1)

        odr.updateRoads(roads)
        odr.resetAndReadjust(byPredecessor=True)

        # Last step, link connection series by curves

        self.createInternalConnectionsForConnectionSeres(
            roads, connectionSeres, junction)

        odr.updateRoads(roads)
        odr.resetAndReadjust(byPredecessor=True)

        return odr

    def createConnectionRoads(self,
                              roads,
                              adjusted=False,
                              areaType=JunctionAreaTypes.SQUARE):

        if adjusted is False:
            # set up x,y positions and headings for the roads around the boundary of the area

            if areaType == JunctionAreaTypes.SQUARE:
                # maximum roads to connect to a side
                maxRoadsPerSide = math.floor(len(roads) / 4) + 1

    def createWithRandomLaneConfigurations(self, numRoads=3):

        raise NotImplementedError()
Esempio n. 10
0
class JunctionHarvester:

    def __init__(self, 
                outputDir, 
                outputPrefix, 
                lastId=0, 
                minAngle = np.pi / 10, 
                maxAngle = np.pi, 
                straightRoadLen = 10,
                esminiPath = None, 
                saveImage = True):
        """The angle between two connected roads are >= self.minAngle <= self.maxAngle

        Args:
            outputDir ([type]): [description]
            outputPrefix ([type]): [description]
            lastId (int, optional): [description]. Defaults to 0.
            minAngle ([type], optional): [description]. Defaults to np.pi/10.
            maxAngle ([type], optional): [description]. Defaults to np.pi.
            straightRoadLen : used both for normal and connection roads
            esminiPath: Path to esmini to generate images for roads.
        """

        self.destinationPrefix = os.path.join(outputDir, outputPrefix)
        self.minAngle = minAngle
        self.maxAngle = maxAngle
        self.lastId = lastId
        self.straightRoadLen = straightRoadLen

        self.roadBuilder = RoadBuilder()
        self.straightRoadBuilder = StraightRoadBuilder()

        self.junctionBuilder = JunctionBuilder(self.roadBuilder)
        self.sequentialJunctionBuilder = SequentialJunctionBuilder(self.roadBuilder, self.straightRoadLen, minAngle=minAngle, maxAngle=maxAngle)

        self.junctionMerger = JunctionMerger(outputDir, outputPrefix, lastId)

        self.configuration = Configuration()

        if esminiPath is None:
            self.esminiPath = self.configuration.get("esminipath")
        else:
            self.esminiPath = esminiPath

        self.saveImage = saveImage

        if os.path.isdir(self.esminiPath) is False:
            logging.warn(f"Esmini path not found {self.esminiPath}. Will break if you try to save images using harvester.")

        pass


    

    def getOutputPath(self, fname):
        return self.destinationPrefix + fname + '.xodr'

    
    def createOdr(self, name, roads, junctions):

        return extensions.createOdr(name, roads, junctions)
        
        # odr = extensions.ExtendedOpenDrive(name)
        # for r in roads:
        #     odr.add_road(r)
        
        # for junction in junctions:
        #     odr.add_junction(junction)

        # print(f"starting adjustment. May freeze!!!!!!!!!!!!!")
        # odr.adjust_roads_and_lanes()

        # return odr


    def harvest2ways2Lanes(self, stepAngle=np.pi/20, maxTries = 100, seed=39):
        """We create junctions of two roads. Will create at least one road per angle.

        Args:
            stepAngle ([type], optional): used to generate angles between roads in conjunction with min and max angles. Defaults to np.pi/20.
            maxTries (int, optional): maximum number of junctions will be maxTries. Defaults to 1000.
            seed (int, optional): defaults to 39
        """
        np.random.seed(seed)
        # for each angle
        countCreated = 0
        roadsPerAngle = self.getRoadsPerAngle(maxTries, stepAngle)

        angleBetweenRoads = self.minAngle 
        odrObjectsPerAngle = {} # we will save the odrs keyed by angles

        while (countCreated < maxTries and angleBetweenRoads < self.maxAngle ):

            odrObjects = self.randomSome2ways2Lanes(angleBetweenRoads, roadsPerAngle)
            odrObjectsPerAngle[str(angleBetweenRoads)] = odrObjects
            countCreated += roadsPerAngle
            angleBetweenRoads += stepAngle
        
        print(f"created {countCreated} roads")

        # Save the odrs

        with(open(self.destinationPrefix + "harvested2R2LOrds.dill", "wb")) as f:
            dill.dump(odrObjectsPerAngle, f)
            print("Odr objects saved to " + self.destinationPrefix + "harvested2R2LOrds.dill" )

        pass


    def getRoadsPerAngle(self, maxTries, stepAngle):
        roadsPerAngle = round((maxTries * stepAngle)/(self.maxAngle - self.minAngle))
        if roadsPerAngle == 0:
            roadsPerAngle = 1
        return roadsPerAngle

    
    def randomSome2ways2Lanes(self, angleBetweenRoads, numberOfRoads):
        """Creates 2way junctions where the connected roads have fixed angle

        Args:
            angleBetweenRoads ([type]): The angle between the roads. The connecting road is a curve that ensures the angle is preserved.
            numberOfRoads ([type]): number of roads to be generated

        Returns:
            [type]: [description]
        """

        print( f"randomSome2ways2Lanes: creating {numberOfRoads} for angle: {math.degrees(angleBetweenRoads)}")
        odrObjects = []
        for i in range( numberOfRoads):
            odr = self.random2ways2Lanes(angleBetweenRoads)
            odrObjects.append(odr)

            # 1. save the xml file
            fname = "2R2L_" + str(round(math.degrees(angleBetweenRoads))) + "_no" + str(i)
            xmlPath = self.getOutputPath(fname)
            odr.write_xml(xmlPath)

            # 2. save image
            if self.saveImage is True:
                extensions.saveRoadImageFromFile(xmlPath, self.esminiPath)

        return odrObjects


    def random2ways2Lanes(self, angleBetweenRoads):
        # print( f"random2ways2Lanes: creating a road network for angle: {math.degrees(angleBetweenRoads)}")
        roads = []
        roads.append(pyodrx.create_straight_road(0, length=self.straightRoadLen)) # cannot reuse roads due to some references to links cannot be reinitialized with pyodrx lib.
        roads.append(self.createRandomConnectionConnectionRoad(1, angleBetweenRoads))
        roads.append(pyodrx.create_straight_road(2, length=self.straightRoadLen))

        self.link3RoadsWithMidAsJunction(roads)
        junction = self.create2RoadJunction(roads)

        self.lastId += 1

        odrName = 'R2_L2_' + str(self.lastId)
        odr = self.createOdr(odrName, roads, [junction])

        return odr


    def createRandomConnectionConnectionRoad(self, connectionRoadId, angleBetweenRoads):
        """The magic connectionRoad

        Args:
            angleBetweenRoads ([type]): The angle between the roads which this connectionRoad is suppose to connect together
            connectionRoadId ([type]): id to be assigned to the new connection road.
        """

        if angleBetweenRoads > 3.05: # when it's greater than 174 degrees, create a straight road
            return pyodrx.create_straight_road(connectionRoadId, length=self.straightRoadLen, junction=1)

        connectionRoad = self.roadBuilder.createRandomCurve(connectionRoadId, angleBetweenRoads, isJunction=True)
        return connectionRoad


    def link3RoadsWithMidAsJunction(self, roads):
        roads[0].add_successor(pyodrx.ElementType.junction,1, pyodrx.ContactPoint.start)
        self.linkInsideRoad(roads[1], 0, 2)
        roads[2].add_predecessor(pyodrx.ElementType.junction,1, pyodrx.ContactPoint.end)


    def linkInsideRoad(self, connectionRoad, predecessorId, successorId):
        connectionRoad.add_predecessor(pyodrx.ElementType.road, predecessorId, pyodrx.ContactPoint.end)
        connectionRoad.add_successor(pyodrx.ElementType.road, successorId, pyodrx.ContactPoint.start)

        pass

    def create2RoadJunction(self, roads):
        """[summary]

        Args:
            roads ([type]): 3 roads with mid as the junction type.
        """

        connection = self.connect2LaneRoads(0, 1)
        junction = pyodrx.Junction('test',1)
        junction.add_connection(connection)

        return junction

    
    def connect2LaneRoads(self, incomingRoadId, connectionRoadId,):
        """Assumes no center lane offset.

        Args:
            incomingRoadId ([type]): [description]
            connectionRoadId ([type]): [description]
        """
        connection = pyodrx.Connection(incomingRoadId, connectionRoadId, pyodrx.ContactPoint.start)
        connection.add_lanelink(-1,-1)
        return connection

        
    def harvest3WayJunctionsFrom2Ways(self, ingredientsFile, maxTries = 100, randomizeAngleSelection = True):
        """Merges 2 way junctions into 3 ways.

        Args:
            ingredientsFile ([type]): [description]
            maxTries (int, optional): [description]. Defaults to 100.
            randomizeAngleSelection (bool, optional): If True it will pick 2R roads randomly without any angle filtering. If false, it will only connect 2R roads which have different angles. Defaults to True.

        Raises:
            NotImplementedError: [description]
        """
        with(open(ingredientsFile, 'rb')) as f:
            odrDic = dill.load(f)

        selectedOdrs = None
        generatedOdrs = []
        if randomizeAngleSelection:
            odrList = []
            for angleOdrList in odrDic.values():
                odrList += angleOdrList

            numberOfOds = len(odrList)

            for _ in range(maxTries):
                try:
                    selectedOdrs = [odrList[np.random.choice(numberOfOds)], odrList[np.random.choice(numberOfOds)]]
                    newOdr = self.junctionMerger.merge2R2L(selectedOdrs)
                    generatedOdrs.append(newOdr)
                    self.lastId += 1

                except IncompatibleRoadsException:
                    continue
                
        else: 
            # TODO angle permutation for merging 2ways
            raise NotImplementedError("Angle permutation is not implemented yet")

    
        
        with(open(self.destinationPrefix + "harvested3R2LOrds.dill", "wb")) as f:
            dill.dump(generatedOdrs, f)
            print("Odr objects saved to " + self.destinationPrefix + "_harvested3R2LOrds.dill" )

        pass 


    def harvestByPainting2L(self, maxNumberOfRoadsPerJunction, triesPerRoadCount, show=False):
        """[summary]

        Args:
            maxNumberOfRoadsPerJunction ([type]): [description]
            triesPerRoadCount ([type]): number of junctions to be created for each set of roads. for 3 roads, we will try triesPerRoadCount, for 4 roads the same.
            save (bool, optional): [description]. Defaults to True.
        """

        odrs = []
        
        for numRoads in range(3, maxNumberOfRoadsPerJunction + 1):
            for _ in range(triesPerRoadCount):
                odr = self.sequentialJunctionBuilder.drawLikeAPainter2L(self.lastId, numRoads)
                self.lastId += 1

                if show:
                    extensions.view_road(odr, os.path.join('..', self.esminiPath))
                    
                xmlPath = self.getOutputPath(odr.name)
                odr.write_xml(xmlPath)
                # 2. save image
                if self.saveImage is True:
                    extensions.saveRoadImageFromFile(xmlPath, self.esminiPath)
                
                odrs.append(odr)
        
        
        
        with(open(self.destinationPrefix + "harvestedByPainting2L.dill", "wb")) as f:
            dill.dump(odrs, f)
            print("Odr objects saved to " + self.destinationPrefix + "harvestedByPainting2L.dill" )

        pass