Subversion Repositories mkgmap

Rev

Rev 3649 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2008 Steve Ratcliffe
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *
 * Author: Steve Ratcliffe
 * Create date: 13-Jul-2008
 */

package uk.me.parabola.imgfmt.app.net;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;

import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.CoordNode;
import uk.me.parabola.log.Logger;
import uk.me.parabola.util.EnhancedProperties;

/**
 * This holds the road network.  That is all the roads and the nodes
 * that connect them together.
 *
 * @see <a href="http://www.movable-type.co.uk/scripts/latlong.html">Distance / bearing calculations</a>
 * @author Steve Ratcliffe
 */

public class RoadNetwork {
        private static final Logger log = Logger.getLogger(RoadNetwork.class);

        private final static int MAX_RESTRICTIONS_ARCS = 7;
        private final Map<Integer, RouteNode> nodes = new LinkedHashMap<>();

        // boundary nodes
        // a node should be in here if the nodes boundary flag is set
        private final List<RouteNode> boundary = new ArrayList<>();
        private final List<RoadDef> roadDefs = new ArrayList<>();
        private List<RouteCenter> centers = new ArrayList<>();
        private AngleChecker angleChecker = new AngleChecker();

        private boolean checkRoundabouts;
        private boolean checkRoundaboutFlares;
        private int maxFlareLengthRatio ;
        private boolean reportSimilarArcs;

        public void config(EnhancedProperties props) {
                checkRoundabouts = props.getProperty("check-roundabouts", false);
                checkRoundaboutFlares = props.getProperty("check-roundabout-flares", false);
                maxFlareLengthRatio = props.getProperty("max-flare-length-ratio", 0);
                reportSimilarArcs = props.getProperty("report-similar-arcs", false);
                angleChecker.config(props);
        }

        public void addRoad(RoadDef roadDef, List<Coord> coordList) {
                roadDefs.add(roadDef);

                CoordNode lastCoord = null;
                int lastIndex = 0;
                double roadLength = 0;
                double arcLength = 0;
                int pointsHash = 0;

                int npoints = coordList.size();
                int numCoordNodes = 0;
                boolean hasInternalNodes = false;
                int numNumberNodes = 0;
                BitSet nodeFlags = new BitSet();
                for (int index = 0; index < npoints; index++) {
                        Coord co = coordList.get(index);
                        int id = co.getId();

                        if (id != 0){
                                nodeFlags.set(numNumberNodes);
                                ++numCoordNodes;
                                if(index > 0 && index < npoints - 1)
                                        hasInternalNodes = true;
                        }
                        if (co.isNumberNode())
                                ++numNumberNodes;
                        if (index == 0){
                                if (id == 0)
                                        roadDef.setStartsWithNode(false);
                               
                        } else {
                                double d = co.distance(coordList.get(index-1));
                                arcLength += d;
                                roadLength += d;
                        }
                        if (roadDef.skipAddToNOD())
                                continue;
                       
                        pointsHash += co.hashCode();

                        if (id == 0)
                                // not a routing node
                                continue;

                        // The next coord determines the heading
                        // If this is the not the first node, then create an arc from
                        // the previous node to this one (and back again).
                        if (lastCoord != null) {
                                int lastId = lastCoord.getId();
                                if(log.isDebugEnabled()) {
                                        log.debug("lastId = " + lastId + " curId = " + id);
                                        log.debug("from " + lastCoord.toDegreeString()
                                                          + " to " + co.toDegreeString());
                                        log.debug("arclength=" + arcLength + " roadlength=" + roadLength);
                                }

                                RouteNode node1 = getOrAddNode(lastId, lastCoord);
                                RouteNode node2 = getOrAddNode(id, co);
                                if(node1 == node2)
                                        log.error("Road " + roadDef + " contains consecutive identical nodes at " + co.toOSMURL() + " - routing will be broken");
                                else if(arcLength == 0)
                                        log.warn("Road " + roadDef + " contains zero length arc at " + co.toOSMURL());

                                Coord forwardBearingPoint = coordList.get(lastIndex + 1);
                                if(lastCoord.equals(forwardBearingPoint) || forwardBearingPoint.isAddedNumberNode()) {
                                        // bearing point is too close to last node to be
                                        // useful - try some more points
                                        for(int bi = lastIndex + 2; bi <= index; ++bi) {
                                                Coord coTest = coordList.get(bi);
                                                if (coTest.isAddedNumberNode() || lastCoord.equals(coTest))
                                                        continue;
                                                forwardBearingPoint = coTest;
                                                break;
                                        }
                                }
                                Coord reverseBearingPoint = coordList.get(index - 1);
                                if(co.equals(reverseBearingPoint) || reverseBearingPoint.isAddedNumberNode()) {
                                        // bearing point is too close to this node to be
                                        // useful - try some more points
                                        for(int bi = index - 2; bi >= lastIndex; --bi) {
                                                Coord coTest = coordList.get(bi);
                                                if (coTest.isAddedNumberNode() || co.equals(coTest))
                                                        continue;
                                                reverseBearingPoint = coTest;
                                                break;
                                        }
                                }
                               
                                double forwardInitialBearing = lastCoord.bearingTo(forwardBearingPoint);
                                double forwardDirectBearing = (co == forwardBearingPoint) ? forwardInitialBearing: lastCoord.bearingTo(co);

                                double reverseInitialBearing = co.bearingTo(reverseBearingPoint);
                                double directLength = (lastIndex + 1 == index) ? arcLength : lastCoord.distance(co);
                                double reverseDirectBearing = 0;
                                if (directLength > 0){
                                        // bearing on rhumb line is a constant, so we can simply revert
                                        reverseDirectBearing = (forwardDirectBearing <= 0) ? 180 + forwardDirectBearing: -(180 - forwardDirectBearing) % 180.0;
                                }
                                // Create forward arc from node1 to node2
                                RouteArc arc = new RouteArc(roadDef,
                                                                                        node1,
                                                                                        node2,
                                                                                        forwardInitialBearing,
                                                                                        forwardDirectBearing,
                                                                                        arcLength,
                                                                                        arcLength,
                                                                                        directLength,
                                                                                        pointsHash);
                                arc.setForward();
                                node1.addArc(arc);
                               
                                // Create the reverse arc
                                RouteArc reverseArc = new RouteArc(roadDef,
                                                                   node2, node1,
                                                                   reverseInitialBearing,
                                                                   reverseDirectBearing,
                                                                   arcLength,
                                                                   arcLength,
                                                                   directLength,
                                                                   pointsHash);
                                node2.addArc(reverseArc);
                                // link the two arcs
                                arc.setReverseArc(reverseArc);
                                reverseArc.setReverseArc(arc);
                        } else {
                                // This is the first node in the road
                                roadDef.setNode(getOrAddNode(id, co));
                        }

                        lastCoord = (CoordNode) co;
                        lastIndex = index;
                        arcLength = 0;
                        pointsHash = co.hashCode();
                }
                if (roadDef.hasHouseNumbers()){
                        // we ignore number nodes when we have no house numbers
                        if (numCoordNodes < numNumberNodes)
                                hasInternalNodes = true;
                        roadDef.setNumNodes(numNumberNodes);
                        roadDef.setNod2BitSet(nodeFlags);
                } else {
                        roadDef.setNumNodes(numCoordNodes);
                }
                if (hasInternalNodes)
                        roadDef.setInternalNodes(true);
                roadDef.setLength(roadLength);
        }

        private RouteNode getOrAddNode(int id, Coord coord) {
                RouteNode node = nodes.get(id);
                if (node == null) {
                        node = new RouteNode(coord);
                        nodes.put(id, node);
                        if (node.isBoundary())
                                boundary.add(node);
                }
                return node;
        }

        public List<RoadDef> getRoadDefs() {
                return roadDefs;
        }

        /**
         * Split the network into RouteCenters.
         *
         * The resulting centers must satisfy several constraints,
         * documented in NOD1Part.
         */

        private void splitCenters() {
                if (nodes.isEmpty())
                        return;
                assert centers.isEmpty() : "already subdivided into centers";

                // sort nodes by NodeGroup
                List<RouteNode> nodeList = new ArrayList<>(nodes.values());
                nodes.clear(); // return to GC
                for (int group = 0; group <= 4; group++){
                        NOD1Part nod1 = new NOD1Part();
                        int n = 0;
                        for (RouteNode node : nodeList) {
                                if (node.getGroup() != group)
                                        continue;
                                if(!node.isBoundary()) {
                                        if(checkRoundabouts)
                                                node.checkRoundabouts();
                                        if(checkRoundaboutFlares)
                                                node.checkRoundaboutFlares(maxFlareLengthRatio);
                                        if(reportSimilarArcs)
                                                node.reportSimilarArcs();
                                }
                               
                                nod1.addNode(node);
                                n++;
                        }
                        if (n > 0)
                                centers.addAll(nod1.subdivide());
                }
        }

        public List<RouteCenter> getCenters() {
                if (centers.isEmpty()){
                        angleChecker.check(nodes);
                        addArcsToMajorRoads();
                        splitCenters();
                }
                return centers;
        }

        /**
         * add indirect arcs for each road class (in descending order)
         */

        private void addArcsToMajorRoads() {
                long t1 = System.currentTimeMillis();
               
                for (RoadDef rd: roadDefs){
                        if (rd.skipAddToNOD())
                                continue;
                        if (rd.getRoadClass() >= 1)
                                rd.getNode().addArcsToMajorRoads(rd);
                }
                log.info(" added major road arcs in " + (System.currentTimeMillis() - t1) + " ms");
        }

        /**
         * Get the list of nodes on the boundary of the network.
         *
         * Currently empty.
         */

        public List<RouteNode> getBoundary() {
                return boundary;
        }

        /**
         * One restriction forbids to travel a specific combination of arcs.
         * We know two kinds: 3 nodes with two arcs and one via node or 4 nodes with 3 arcs
         * and two via nodes. Maybe more nodes are possible, but we don't know for sure how
         * to write them (2014-04-02).
         * Depending on the data in grr we create one or more such restrictions.
         * A restriction with 4 (or more) nodes is added to each via node.    
         *
         * The OSM restriction gives a from way id and a to way id and one or more
         * via nodes. It is possible that the to-way is a loop, so we have to identify
         * the correct arc.
         * @param grr the object that holds the details about the route restriction
         */

        public int addRestriction(GeneralRouteRestriction grr) {
                if (grr.getType() == GeneralRouteRestriction.RestrType.TYPE_NO_TROUGH)
                        return addNoThroughRoute(grr);
                String sourceDesc = grr.getSourceDesc();
               
                List<RouteNode> viaNodes = new ArrayList<>();
                for (CoordNode via : grr.getViaNodes()){
                        RouteNode vn = nodes.get(via.getId());
                        if (vn == null){
                                log.error(sourceDesc, "can't locate 'via' RouteNode with id", via.getId());
                                return 0;
                        }
                        viaNodes.add(vn);
                }
               
                int firstViaId = grr.getViaNodes().get(0).getId();
                int lastViaId = grr.getViaNodes().get(grr.getViaNodes().size()-1).getId();
                RouteNode firstViaNode = nodes.get(firstViaId);
                RouteNode lastViaNode = nodes.get(lastViaId);
                List<List<RouteArc>> viaArcsList = new ArrayList<>();
                if (grr.getViaNodes().size() != grr.getViaWayIds().size() + 1){
                        log.error(sourceDesc, "internal error: number of via nodes and via ways doesn't fit");
                        return 0;
                }
                for (int i = 1; i < grr.getViaNodes().size(); i++){
                        RouteNode vn = viaNodes.get(i-1);
                        Long viaWayId = grr.getViaWayIds().get(i-1);
                        List<RouteArc> viaArcs = vn.getDirectArcsTo(viaNodes.get(i), viaWayId);
                        if (viaArcs.isEmpty()){
                                log.error(sourceDesc, "can't locate arc from 'via' node at",vn.getCoord().toOSMURL(),"to next 'via' node on way",viaWayId);
                                return 0;
                        }
                        viaArcsList.add(viaArcs);
                }
               
                // determine the from node and arc(s)
                int fromId = 0;
                RouteNode fn = null;
                if (grr.getFromNode() != null){
                        fromId = grr.getFromNode().getId();
                        // polish input data provides id
                        fn = nodes.get(fromId);
                        if (fn == null ){
                                log.error(sourceDesc, "can't locate 'from' RouteNode with id", fromId);
                                return 0;
                        }
                } else {
                        List<RouteArc> possibleFromArcs = firstViaNode.getDirectArcsOnWay(grr.getFromWayId());
                        for (RouteArc arc : possibleFromArcs){
                                if (fn == null)
                                        fn = arc.getDest();
                                else if (fn != arc.getDest()){
                                        log.warn(sourceDesc, "found different 'from' arcs for way",grr.getFromWayId(),"restriction is ignored");
                                        return 0;
                                }
                        }
                        if (fn == null){
                                log.warn(sourceDesc, "can't locate 'from' RouteNode for 'from' way", grr.getFromWayId());
                                return 0;
                        }
                        fromId = fn.getCoord().getId();
                }
                List<RouteArc> fromArcs = fn.getDirectArcsTo(firstViaNode, grr.getFromWayId());
                if (fromArcs.isEmpty()){
                        log.error(sourceDesc, "can't locate arc from 'from' node ",fromId,"to 'via' node",firstViaId,"on way",grr.getFromWayId());
                        return 0;
                }
               
                // a bit more complex: determine the to-node and arc(s)
                RouteNode tn = null;
                int toId = 0;
                List<RouteArc> toArcs = new ArrayList<>();
                if (grr.getToNode() != null){
                        // polish input data provides id
                        toId = grr.getToNode().getId();
                        tn = nodes.get(toId);
                        if (tn == null ){
                                log.error(sourceDesc, "can't locate 'to' RouteNode with id", toId);
                                return 0;
                        }
                } else {
                        // we can have multiple arcs between last via node and to node. The
                        // arcs can be on the same OSM way or on different OSM ways.
                        // We can have multiple arcs with different RoadDef objects that refer to the same
                        // OSM way id. The direction indicator tells us what arc is probably meant.
                        List<RouteArc> possibleToArcs = lastViaNode.getDirectArcsOnWay(grr.getToWayId());
                        RouteArc fromArc = fromArcs.get(0);

                        boolean ignoreAngle = false;
                        if (fromArc.getLengthInMeter() <= 0.0001)
                                ignoreAngle = true;
                        if (grr.getDirIndicator() == '?')
                                ignoreAngle = true;
                        log.info(sourceDesc, "found", possibleToArcs.size(), "candidates for to-arc");

                        // group the available arcs by angle
                        Map<Integer, List<RouteArc>> angleMap = new TreeMap<>();
                        for (RouteArc arc : possibleToArcs){
                                if (arc.getLengthInMeter() <= 0.0001)
                                        ignoreAngle = true;
                                Integer angle = Math.round(getAngle(fromArc, arc));
                                List<RouteArc> list = angleMap.get(angle);
                                if (list == null){
                                        list = new ArrayList<>();
                                        angleMap.put(angle, list);
                                }
                                list.add(arc);
                        }

                        // find the group that fits best
                        Iterator<Entry<Integer, List<RouteArc>>> iter = angleMap.entrySet().iterator();
                        Integer bestAngle = null;
                        while (iter.hasNext()){
                                Entry<Integer, List<RouteArc>> entry = iter.next();
                                if (ignoreAngle || matchDirectionInfo(entry.getKey(), grr.getDirIndicator()) ){
                                        if (bestAngle == null)
                                                bestAngle = entry.getKey();
                                        else {
                                                bestAngle = getBetterAngle(bestAngle, entry.getKey(), grr.getDirIndicator());
                                        }
                                }
                        }
                       
                        if (bestAngle == null){
                                log.warn(sourceDesc,"the angle of the from and to way don't match the restriction");
                                return 0;
                        }
                        toArcs = angleMap.get(bestAngle);
                }
                if (toArcs.isEmpty()){
                        log.error(sourceDesc, "can't locate arc from 'via' node ",lastViaId,"to 'to' node",toId,"on way",grr.getToWayId());
                        return 0;
                }
               
                List<RouteArc> badArcs = new ArrayList<>();
               
                if (grr.getType() == GeneralRouteRestriction.RestrType.TYPE_NOT){
                        for (RouteArc toArc: toArcs){
                                badArcs.add(toArc);
                        }
                }
                else if (grr.getType() == GeneralRouteRestriction.RestrType.TYPE_ONLY){
                        // this is the inverse logic, grr gives the allowed path, we have to find the others
                        for (RouteArc badArc : lastViaNode.arcsIteration()){
                                if (!badArc.isDirect() || toArcs.contains(badArc))
                                        continue;
                                badArcs.add(badArc);
                        }
                        if (badArcs.isEmpty()){
                                log.warn(sourceDesc, "restriction ignored because it has no effect");
                                return 0;
                        }
                }
               
                // create all possible paths for which the restriction applies
                List<List<RouteArc>> arcLists = new ArrayList<>();
                arcLists.add(fromArcs);
                arcLists.addAll(viaArcsList);
                arcLists.add(badArcs);
                if (arcLists.size() > MAX_RESTRICTIONS_ARCS){
                        log.warn(sourceDesc, "has more than", MAX_RESTRICTIONS_ARCS, "arcs, this is not supported");
                        return 0;
                }
               
                // remove arcs which cannot be travelled by the vehicles listed in the restriction
                for (int i = 0; i < arcLists.size(); i++){
                        List<RouteArc> arcs =  arcLists.get(i);
                        int countNoEffect = 0;
                        int countOneway= 0;
                        for (int j = arcs.size()-1; j >= 0; --j){
                                RouteArc arc = arcs.get(j);
                                if (isUsable(arc.getRoadDef().getAccess(), grr.getExceptionMask()) == false){
                                        countNoEffect++;
                                        arcs.remove(j);
                                }
                                else if (arc.getRoadDef().isOneway()){
                                        if (!arc.isForward()){
                                                countOneway++;
                                                arcs.remove(j);
                                        }
                                }
                        }
                        String arcType = null;
                        if (arcs.isEmpty()){
                                if (i == 0)
                                        arcType = "from way is";
                                else if (i == arcLists.size()-1){
                                        if (grr.getType() == GeneralRouteRestriction.RestrType.TYPE_ONLY)
                                                arcType = "all possible other ways are";
                                        else
                                                arcType = "to way is";
                                }
                                else
                                        arcType = "via way is";
                                String reason;
                                if (countNoEffect > 0 & countOneway > 0)
                                        reason = "wrong direction in oneway or not accessible for restricted vehicles";
                                else if (countNoEffect > 0)
                                        reason = "not accessible for restricted vehicles";
                                else
                                        reason = "wrong direction in oneway";
                                log.warn(sourceDesc, "restriction ignored because",arcType,reason);
                                return 0;
                        }
                }
                if (viaNodes.contains(fn)){
                        log.warn(sourceDesc, "restriction not written because from node appears also as via node");
                        return 0;
                }
                // determine all possible combinations of arcs. In most cases,
                // this will be 0 or one, but if the style creates multiple roads for one
                // OSM way, this can be a larger number
                int numCombis = 1;
                int [] indexes = new int[arcLists.size()];
                for (int i = 0; i < indexes.length; i++){
                        List<RouteArc> arcs =  arcLists.get(i);
                        numCombis *= arcs.size();
                }
                List<RouteArc> path = new ArrayList<>();
                int added = 0;
                for (int i = 0; i < numCombis; i++){
                        for (RouteNode vn : viaNodes){
                                path.clear();
                                boolean viaNodeFound = false;
                                byte pathNoAccessMask = 0;
                                for (int j = 0; j < indexes.length; j++){
                                        RouteArc arc = arcLists.get(j).get(indexes[j]);
                                        if (arc.getDest() == vn || viaNodeFound == false){
                                                arc = arc.getReverseArc();
                                        }
                                        if (arc.getSource() == vn)
                                                viaNodeFound = true;
                                        if (arc.getDest() == vn){
                                                if (added > 0)
                                                        log.error(sourceDesc, "restriction incompletely written because dest in arc is via node");
                                                else
                                                        log.warn(sourceDesc, "restriction not written because dest in arc is via node");
                                                return added;
                                        }
                                        pathNoAccessMask |= ~arc.getRoadDef().getAccess();
                                        path.add(arc);
                                }
                                byte pathAccessMask = (byte)~pathNoAccessMask;
                                if (isUsable(pathAccessMask, grr.getExceptionMask())){
                                        vn.addRestriction(new RouteRestriction(vn, path, grr.getExceptionMask()));
                                        ++added;
                                }
                        }
                        // get next combination of arcs
                        ++indexes[indexes.length-1];
                        for (int j = indexes.length-1; j > 0; --j){
                                if (indexes[j] >= arcLists.get(j).size()){
                                        indexes[j] = 0;
                                        indexes[j-1]++;
                                }
                        }
                }

                // double check
                if (indexes[0] != arcLists.get(0).size())
                        log.error(sourceDesc, " failed to generate all possible paths");
                log.info(sourceDesc, "added",added,"route restriction(s) to img file");
                return added;
        }

        /**
         * Compare the disallowed vehicles for the path with the exceptions from the restriction
         * @param roadNoAccess
         * @param exceptionMask
         * @return
         */

        private static boolean isUsable(byte roadAccess, byte exceptionMask) {
                if ((roadAccess & (byte) ~exceptionMask) == 0)
                        return false; // no allowed vehicle is concerned by this restriction
                return true;
        }

        private int addNoThroughRoute(GeneralRouteRestriction grr) {
                assert grr.getViaNodes() != null;
                assert grr.getViaNodes().size() == 1;
                int viaId = grr.getViaNodes().get(0).getId();
                RouteNode vn = nodes.get(viaId);
                if (vn == null){
                        log.error(grr.getSourceDesc(), "can't locate 'via' RouteNode with id", viaId);
                        return 0;
                }
                int added = 0;
               
                for (RouteArc out: vn.arcsIteration()){
                        if (!out.isDirect())
                                continue;
                        for (RouteArc in: vn.arcsIteration()){
                                if (!in.isDirect() || in == out || in.getDest() == out.getDest())
                                        continue;
                                byte pathAccessMask = (byte) (out.getRoadDef().getAccess() & in.getRoadDef().getAccess());
                                if (isUsable(pathAccessMask, grr.getExceptionMask())){
                                        vn.addRestriction(new RouteRestriction(vn, Arrays.asList(in,out), grr.getExceptionMask()));
                                        added++;
                                } else {
                                        if (log.isDebugEnabled())
                                                log.debug(grr.getSourceDesc(),"ignored no-through-route",in,"to",out);
                                }
                        }
                }
                return added;
        }
       
        public void addThroughRoute(int junctionNodeId, long roadIdA, long roadIdB) {
                RouteNode node = nodes.get(junctionNodeId);
                assert node != null :  "Can't find node with id " + junctionNodeId;

                node.addThroughRoute(roadIdA, roadIdB);
        }
       
        /**
         * Calculate the "angle" between to arcs. The arcs may not be connected.
         * We do this by "virtually" moving the toArc so that its source
         * node lies on the destination node of the from arc.
         * This makes only sense if move is not over a large distance, we assume that this
         * is the case as via ways should be short.
         * @param fromArc arc with from node as source and first via node as destination
         * @param toArc arc with last via node as source
         * @return angle at in degree [-180;180]
         */

        private static float getAngle(RouteArc fromArc, RouteArc toArc){
                // note that the values do not depend on the isForward() attribute
                float headingFrom = fromArc.getFinalHeading();
                float headingTo = toArc.getInitialHeading();
                float angle = headingTo - headingFrom;
                while(angle > 180)
                        angle -= 360;
                while(angle < -180)
                        angle += 360;
                return angle;
        }
       
        /**
         * Find the angle that comes closer to the direction indicated.
         *
         * @param angle1 1st angle -180:180 degrees
         * @param angle2 2nd angle -180:180 degrees
         * @param dirIndicator l:left, r:right, u:u_turn, s: straight_on
         * @return
         */

        private static Integer getBetterAngle (Integer angle1, Integer angle2, char dirIndicator){
                switch (dirIndicator){
                case 'l':
                        if (Math.abs(-90-angle2) < Math.abs(-90-angle1))
                                return angle2; // closer to -90
                        break;
                case 'r':
                        if (Math.abs(90-angle2) < Math.abs(90-angle1))
                                return angle2; // closer to 90
                        break;
                case 'u':
                        double d1 = (angle1 < 0 ) ? -180-angle1 : 180-angle1;
                        double d2 = (angle2 < 0 ) ? -180-angle2 : 180-angle2;
                        if (Math.abs(d2) < Math.abs(d1))
                                return angle2; // closer to -180
                        break;
                case 's':
                        if (Math.abs(angle2) < Math.abs(angle1))
                                return angle2; // closer to 0
                        break;
                }
               
                return angle1;
        }
       
        /**
         * Check if angle is in the range indicated by the direction
         * @param angle the angle -180:180 degrees
         * @param dirIndicator l:left, r:right, u:u_turn, s: straight_on
         * @return
         */

        private static boolean matchDirectionInfo (float angle, char dirIndicator){
                switch (dirIndicator){
                case 'l':
                        if (angle < -3 && angle > - 177)
                                return true;
                        break;
                case 'r':
                        if (angle > 3 && angle < 177)
                                return true;
                        break;
                case 'u':
                        if (angle < -87 || angle > 93)
                                return true;
                        break;
                case 's':
                        if (angle > -87 && angle < 87)
                                return true;
                        break;
                case '?':
                        return true;
                }
                return false;
        }
       
       
}