From 91e62f1164f030c82fd439851cf01f95da6f7099 Mon Sep 17 00:00:00 2001 From: Arndt Brenschede Date: Sat, 18 Jan 2014 15:29:05 +0100 Subject: [PATCH] initial commit of BRouter Version 0.98 --- README.md | 6 + brouter-core/pom.xml | 31 + .../java/btools/router/MatchedWaypoint.java | 56 + .../src/main/java/btools/router/OpenSet.java | 172 ++++ .../main/java/btools/router/OsmNodeNamed.java | 33 + .../src/main/java/btools/router/OsmPath.java | 371 +++++++ .../java/btools/router/OsmPathElement.java | 113 ++ .../src/main/java/btools/router/OsmTrack.java | 365 +++++++ .../java/btools/router/RoutingContext.java | 241 +++++ .../java/btools/router/RoutingEngine.java | 968 ++++++++++++++++++ .../btools/router/RoutingMessageHandler.java | 28 + brouter-expressions/pom.xml | 13 + .../java/btools/expressions/BExpression.java | 173 ++++ .../expressions/BExpressionContext.java | 620 +++++++++++ .../expressions/BExpressionLookupValue.java | 66 ++ .../expressions/BExpressionReceiver.java | 7 + brouter-map-creator/pom.xml | 35 + .../btools/mapcreator/MapCreatorBase.java | 166 +++ .../java/btools/mapcreator/NodeCutter.java | 87 ++ .../main/java/btools/mapcreator/NodeData.java | 55 + .../java/btools/mapcreator/NodeFilter.java | 86 ++ .../java/btools/mapcreator/NodeIterator.java | 76 ++ .../java/btools/mapcreator/NodeListener.java | 17 + .../java/btools/mapcreator/OsmCutter.java | 205 ++++ .../main/java/btools/mapcreator/OsmLinkP.java | 29 + .../main/java/btools/mapcreator/OsmNodeP.java | 243 +++++ .../java/btools/mapcreator/OsmNodePT.java | 39 + .../java/btools/mapcreator/OsmParser.java | 240 +++++ .../java/btools/mapcreator/PosUnifier.java | 203 ++++ .../java/btools/mapcreator/RelationData.java | 37 + .../btools/mapcreator/RelationListener.java | 13 + .../main/java/btools/mapcreator/SrtmData.java | 174 ++++ .../java/btools/mapcreator/WayCutter.java | 127 +++ .../java/btools/mapcreator/WayCutter5.java | 146 +++ .../main/java/btools/mapcreator/WayData.java | 62 ++ .../java/btools/mapcreator/WayIterator.java | 76 ++ .../java/btools/mapcreator/WayLinker.java | 289 ++++++ .../java/btools/mapcreator/WayListener.java | 17 + .../btools/mapcreator/MapcreatorTest.java | 70 ++ .../btools/mapcreator/MapcreatorTest.java.BAK | 70 ++ .../src/test/resources/all.brf | 18 + .../src/test/resources/all.brf.BAK | 11 + .../src/test/resources/car-test.brf | 107 ++ .../src/test/resources/dreieich.osm.gz | Bin 0 -> 157316 bytes .../src/test/resources/lookups.dat | 317 ++++++ brouter-mapaccess/pom.xml | 13 + .../java/btools/mapaccess/ByteDataReader.java | 59 ++ .../java/btools/mapaccess/ByteDataWriter.java | 54 + .../btools/mapaccess/DistanceChecker.java | 16 + .../java/btools/mapaccess/MicroCache.java | 232 +++++ .../java/btools/mapaccess/NodesCache.java | 223 ++++ .../main/java/btools/mapaccess/NodesList.java | 14 + .../main/java/btools/mapaccess/OsmFile.java | 67 ++ .../main/java/btools/mapaccess/OsmLink.java | 53 + .../java/btools/mapaccess/OsmLinkHolder.java | 13 + .../main/java/btools/mapaccess/OsmNode.java | 397 +++++++ .../java/btools/mapaccess/OsmNodesMap.java | 109 ++ .../main/java/btools/mapaccess/OsmPos.java | 23 + .../btools/mapaccess/OsmTransferNode.java | 144 +++ brouter-routing-app/AndroidManifest.xml | 28 + brouter-routing-app/classpath | 8 + .../gen/btools/routingapp/BuildConfig.java | 5 + .../btools/routingapp/IBRouterService.java | 136 +++ .../gen/btools/routingapp/R.java | 22 + brouter-routing-app/pom.xml | 36 + brouter-routing-app/project | 33 + brouter-routing-app/project.properties | 11 + .../res/drawable-hdpi/icon.png | Bin 0 -> 11374 bytes .../res/drawable-ldpi/icon.png | Bin 0 -> 3229 bytes .../res/drawable-mdpi/icon.png | Bin 0 -> 5479 bytes brouter-routing-app/res/layout/main.xml | 22 + brouter-routing-app/res/values/strings.xml | 19 + .../btools/routingapp/BRouterActivity.java | 375 +++++++ .../btools/routingapp/BRouterService.java | 146 +++ .../java/btools/routingapp/BRouterView.java | 760 ++++++++++++++ .../java/btools/routingapp/BRouterWorker.java | 139 +++ .../btools/routingapp/CoordinateReader.java | 137 +++ .../routingapp/CoordinateReaderLocus.java | 53 + .../routingapp/CoordinateReaderNone.java | 26 + .../routingapp/CoordinateReaderOrux.java | 52 + .../routingapp/CoordinateReaderOsmAnd.java | 87 ++ .../btools/routingapp/IBRouterService.aidl | 23 + .../btools/routingapp/ServiceModeConfig.java | 50 + .../resources/brouter/profiles2/car-test.brf | 105 ++ .../resources/brouter/profiles2/fastbike.brf | 164 +++ .../resources/brouter/profiles2/lookups.dat | 317 ++++++ .../resources/brouter/profiles2/moped.brf | 119 +++ .../resources/brouter/profiles2/safety.brf | 223 ++++ .../resources/brouter/profiles2/shortest.brf | 89 ++ .../brouter/profiles2/trekking-ignore-cr.brf | 223 ++++ .../brouter/profiles2/trekking-noferries.brf | 223 ++++ .../brouter/profiles2/trekking-nosteps.brf | 223 ++++ .../brouter/profiles2/trekking-steep.brf | 223 ++++ .../resources/brouter/profiles2/trekking.brf | 223 ++++ brouter-server/pom.xml | 55 + .../src/main/java/btools/server/BRouter.java | 132 +++ .../main/java/btools/server/CgiUpload.java | 111 ++ .../main/java/btools/server/RouteServer.java | 267 +++++ .../java/btools/server/ServiceContext.java | 16 + .../btools/server/request/RequestHandler.java | 28 + .../btools/server/request/ServerHandler.java | 149 +++ .../btools/server/request/YoursHandler.java | 69 ++ brouter-util/pom.xml | 21 + .../main/java/btools/util/CompactLongMap.java | 328 ++++++ .../main/java/btools/util/CompactLongSet.java | 220 ++++ .../main/java/btools/util/DenseLongMap.java | 140 +++ .../main/java/btools/util/FrozenLongMap.java | 122 +++ .../main/java/btools/util/FrozenLongSet.java | 81 ++ .../java/btools/util/LazyArrayOfLists.java | 53 + .../src/main/java/btools/util/LongList.java | 43 + .../java/btools/util/TinyDenseLongMap.java | 206 ++++ .../test/java/btools/util/CompactLongMap.java | 328 ++++++ .../test/java/btools/util/CompactLongSet.java | 220 ++++ .../test/java/btools/util/DenseLongMap.java | 140 +++ .../test/java/btools/util/FrozenLongMap.java | 122 +++ .../test/java/btools/util/FrozenLongSet.java | 81 ++ .../java/btools/util/LazyArrayOfLists.java | 53 + .../src/test/java/btools/util/LongList.java | 43 + .../java/btools/util/TinyDenseLongMap.java | 206 ++++ pom.xml | 153 +++ 120 files changed, 15382 insertions(+) create mode 100644 brouter-core/pom.xml create mode 100644 brouter-core/src/main/java/btools/router/MatchedWaypoint.java create mode 100644 brouter-core/src/main/java/btools/router/OpenSet.java create mode 100644 brouter-core/src/main/java/btools/router/OsmNodeNamed.java create mode 100644 brouter-core/src/main/java/btools/router/OsmPath.java create mode 100644 brouter-core/src/main/java/btools/router/OsmPathElement.java create mode 100644 brouter-core/src/main/java/btools/router/OsmTrack.java create mode 100644 brouter-core/src/main/java/btools/router/RoutingContext.java create mode 100644 brouter-core/src/main/java/btools/router/RoutingEngine.java create mode 100644 brouter-core/src/main/java/btools/router/RoutingMessageHandler.java create mode 100644 brouter-expressions/pom.xml create mode 100644 brouter-expressions/src/main/java/btools/expressions/BExpression.java create mode 100644 brouter-expressions/src/main/java/btools/expressions/BExpressionContext.java create mode 100644 brouter-expressions/src/main/java/btools/expressions/BExpressionLookupValue.java create mode 100644 brouter-expressions/src/main/java/btools/expressions/BExpressionReceiver.java create mode 100644 brouter-map-creator/pom.xml create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/MapCreatorBase.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/NodeCutter.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/NodeData.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/NodeFilter.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/NodeIterator.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/NodeListener.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/OsmCutter.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/OsmLinkP.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/OsmNodeP.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/OsmNodePT.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/OsmParser.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/PosUnifier.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/RelationData.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/RelationListener.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/SrtmData.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/WayCutter.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/WayCutter5.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/WayData.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/WayIterator.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/WayLinker.java create mode 100644 brouter-map-creator/src/main/java/btools/mapcreator/WayListener.java create mode 100644 brouter-map-creator/src/test/java/btools/mapcreator/MapcreatorTest.java create mode 100644 brouter-map-creator/src/test/java/btools/mapcreator/MapcreatorTest.java.BAK create mode 100644 brouter-map-creator/src/test/resources/all.brf create mode 100644 brouter-map-creator/src/test/resources/all.brf.BAK create mode 100644 brouter-map-creator/src/test/resources/car-test.brf create mode 100644 brouter-map-creator/src/test/resources/dreieich.osm.gz create mode 100644 brouter-map-creator/src/test/resources/lookups.dat create mode 100644 brouter-mapaccess/pom.xml create mode 100644 brouter-mapaccess/src/main/java/btools/mapaccess/ByteDataReader.java create mode 100644 brouter-mapaccess/src/main/java/btools/mapaccess/ByteDataWriter.java create mode 100644 brouter-mapaccess/src/main/java/btools/mapaccess/DistanceChecker.java create mode 100644 brouter-mapaccess/src/main/java/btools/mapaccess/MicroCache.java create mode 100644 brouter-mapaccess/src/main/java/btools/mapaccess/NodesCache.java create mode 100644 brouter-mapaccess/src/main/java/btools/mapaccess/NodesList.java create mode 100644 brouter-mapaccess/src/main/java/btools/mapaccess/OsmFile.java create mode 100644 brouter-mapaccess/src/main/java/btools/mapaccess/OsmLink.java create mode 100644 brouter-mapaccess/src/main/java/btools/mapaccess/OsmLinkHolder.java create mode 100644 brouter-mapaccess/src/main/java/btools/mapaccess/OsmNode.java create mode 100644 brouter-mapaccess/src/main/java/btools/mapaccess/OsmNodesMap.java create mode 100644 brouter-mapaccess/src/main/java/btools/mapaccess/OsmPos.java create mode 100644 brouter-mapaccess/src/main/java/btools/mapaccess/OsmTransferNode.java create mode 100644 brouter-routing-app/AndroidManifest.xml create mode 100644 brouter-routing-app/classpath create mode 100644 brouter-routing-app/gen/btools/routingapp/BuildConfig.java create mode 100644 brouter-routing-app/gen/btools/routingapp/IBRouterService.java create mode 100644 brouter-routing-app/gen/btools/routingapp/R.java create mode 100644 brouter-routing-app/pom.xml create mode 100644 brouter-routing-app/project create mode 100644 brouter-routing-app/project.properties create mode 100644 brouter-routing-app/res/drawable-hdpi/icon.png create mode 100644 brouter-routing-app/res/drawable-ldpi/icon.png create mode 100644 brouter-routing-app/res/drawable-mdpi/icon.png create mode 100644 brouter-routing-app/res/layout/main.xml create mode 100644 brouter-routing-app/res/values/strings.xml create mode 100644 brouter-routing-app/src/main/java/btools/routingapp/BRouterActivity.java create mode 100644 brouter-routing-app/src/main/java/btools/routingapp/BRouterService.java create mode 100644 brouter-routing-app/src/main/java/btools/routingapp/BRouterView.java create mode 100644 brouter-routing-app/src/main/java/btools/routingapp/BRouterWorker.java create mode 100644 brouter-routing-app/src/main/java/btools/routingapp/CoordinateReader.java create mode 100644 brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderLocus.java create mode 100644 brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderNone.java create mode 100644 brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderOrux.java create mode 100644 brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderOsmAnd.java create mode 100644 brouter-routing-app/src/main/java/btools/routingapp/IBRouterService.aidl create mode 100644 brouter-routing-app/src/main/java/btools/routingapp/ServiceModeConfig.java create mode 100644 brouter-routing-app/src/main/resources/brouter/profiles2/car-test.brf create mode 100644 brouter-routing-app/src/main/resources/brouter/profiles2/fastbike.brf create mode 100644 brouter-routing-app/src/main/resources/brouter/profiles2/lookups.dat create mode 100644 brouter-routing-app/src/main/resources/brouter/profiles2/moped.brf create mode 100644 brouter-routing-app/src/main/resources/brouter/profiles2/safety.brf create mode 100644 brouter-routing-app/src/main/resources/brouter/profiles2/shortest.brf create mode 100644 brouter-routing-app/src/main/resources/brouter/profiles2/trekking-ignore-cr.brf create mode 100644 brouter-routing-app/src/main/resources/brouter/profiles2/trekking-noferries.brf create mode 100644 brouter-routing-app/src/main/resources/brouter/profiles2/trekking-nosteps.brf create mode 100644 brouter-routing-app/src/main/resources/brouter/profiles2/trekking-steep.brf create mode 100644 brouter-routing-app/src/main/resources/brouter/profiles2/trekking.brf create mode 100644 brouter-server/pom.xml create mode 100644 brouter-server/src/main/java/btools/server/BRouter.java create mode 100644 brouter-server/src/main/java/btools/server/CgiUpload.java create mode 100644 brouter-server/src/main/java/btools/server/RouteServer.java create mode 100644 brouter-server/src/main/java/btools/server/ServiceContext.java create mode 100644 brouter-server/src/main/java/btools/server/request/RequestHandler.java create mode 100644 brouter-server/src/main/java/btools/server/request/ServerHandler.java create mode 100644 brouter-server/src/main/java/btools/server/request/YoursHandler.java create mode 100644 brouter-util/pom.xml create mode 100644 brouter-util/src/main/java/btools/util/CompactLongMap.java create mode 100644 brouter-util/src/main/java/btools/util/CompactLongSet.java create mode 100644 brouter-util/src/main/java/btools/util/DenseLongMap.java create mode 100644 brouter-util/src/main/java/btools/util/FrozenLongMap.java create mode 100644 brouter-util/src/main/java/btools/util/FrozenLongSet.java create mode 100644 brouter-util/src/main/java/btools/util/LazyArrayOfLists.java create mode 100644 brouter-util/src/main/java/btools/util/LongList.java create mode 100644 brouter-util/src/main/java/btools/util/TinyDenseLongMap.java create mode 100644 brouter-util/src/test/java/btools/util/CompactLongMap.java create mode 100644 brouter-util/src/test/java/btools/util/CompactLongSet.java create mode 100644 brouter-util/src/test/java/btools/util/DenseLongMap.java create mode 100644 brouter-util/src/test/java/btools/util/FrozenLongMap.java create mode 100644 brouter-util/src/test/java/btools/util/FrozenLongSet.java create mode 100644 brouter-util/src/test/java/btools/util/LazyArrayOfLists.java create mode 100644 brouter-util/src/test/java/btools/util/LongList.java create mode 100644 brouter-util/src/test/java/btools/util/TinyDenseLongMap.java create mode 100644 pom.xml diff --git a/README.md b/README.md index bb2b895..ddb4dbe 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,9 @@ brouter ======= configurable OSM offline router with elevation awareness, Java + Android + +For more infos see http://brensche.de/brouter + +Compile with (Java 6!): + +> mvn clean install -Dandroid.sdk.path= diff --git a/brouter-core/pom.xml b/brouter-core/pom.xml new file mode 100644 index 0000000..e75292b --- /dev/null +++ b/brouter-core/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + org.btools + brouter + 0.98 + ../pom.xml + + brouter-core + jar + + + + org.btools + brouter-util + ${project.version} + + + org.btools + brouter-mapaccess + ${project.version} + + + org.btools + brouter-expressions + ${project.version} + + + diff --git a/brouter-core/src/main/java/btools/router/MatchedWaypoint.java b/brouter-core/src/main/java/btools/router/MatchedWaypoint.java new file mode 100644 index 0000000..cb59d90 --- /dev/null +++ b/brouter-core/src/main/java/btools/router/MatchedWaypoint.java @@ -0,0 +1,56 @@ +/** + * Information on matched way point + * + * @author ab + */ +package btools.router; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import btools.mapaccess.OsmNode; + +final class MatchedWaypoint +{ + public OsmNode node1; + public OsmNode node2; + public OsmNodeNamed crosspoint; + public OsmNodeNamed waypoint; + public double radius; + public int cost; + + public void writeToStream( DataOutput dos ) throws IOException + { + dos.writeInt( node1.ilat ); + dos.writeInt( node1.ilon ); + dos.writeInt( node2.ilat ); + dos.writeInt( node2.ilon ); + dos.writeInt( crosspoint.ilat ); + dos.writeInt( crosspoint.ilon ); + dos.writeInt( waypoint.ilat ); + dos.writeInt( waypoint.ilon ); + dos.writeDouble( radius ); + } + + public static MatchedWaypoint readFromStream( DataInput dis ) throws IOException + { + MatchedWaypoint mwp = new MatchedWaypoint(); + mwp.node1 = new OsmNode(); + mwp.node2 = new OsmNode(); + mwp.crosspoint = new OsmNodeNamed(); + mwp.waypoint = new OsmNodeNamed(); + + mwp.node1.ilat = dis.readInt(); + mwp.node1.ilon = dis.readInt(); + mwp.node2.ilat = dis.readInt(); + mwp.node2.ilon = dis.readInt(); + mwp.crosspoint.ilat = dis.readInt(); + mwp.crosspoint.ilon = dis.readInt(); + mwp.waypoint.ilat = dis.readInt(); + mwp.waypoint.ilon = dis.readInt(); + mwp.radius = dis.readDouble(); + return mwp; + } + +} diff --git a/brouter-core/src/main/java/btools/router/OpenSet.java b/brouter-core/src/main/java/btools/router/OpenSet.java new file mode 100644 index 0000000..77b1340 --- /dev/null +++ b/brouter-core/src/main/java/btools/router/OpenSet.java @@ -0,0 +1,172 @@ +/** + * Implementation for the open-set + * that should be somewhat faster + * and memory-efficient than the original + * version based on java.util.TreeSet + * + * It relies on the two double-linked + * lists implemented in OsmPath + * + * @author ab + */ +package btools.router; + +import btools.mapaccess.OsmNode; + +public class OpenSet +{ + private OsmPath start = new OsmPath(); + private OsmPath index2 = new OsmPath(); + + private int addCount = 0; + + private int size = 0; + + public void clear() + { + start.nextInSet = null; + start.nextInIndexSet = null; + index2.nextInIndexSet = null; + size = 0; + addCount = 0; + } + + public void add( OsmPath path ) + { + int ac = path.adjustedCost; + OsmPath p1 = index2; + + // fast forward along index2 + while( p1.nextInIndexSet != null && p1.nextInIndexSet.adjustedCost < ac ) + { + p1 = p1.nextInIndexSet; + } + if ( p1 == index2 ) + { + p1 = start; + } + + // search using index1 + for(;;) + { + if ( p1.nextInIndexSet != null && p1.nextInIndexSet.adjustedCost < ac ) + { + p1 = p1.nextInIndexSet; + } + else if ( p1.nextInSet != null && p1.nextInSet.adjustedCost < ac ) + { + p1 = p1.nextInSet; + } + else + { + break; + } + } + OsmPath p2 = p1.nextInSet; + + p1.nextInSet = path; + path.prevInSet = p1; + path.nextInSet = p2; + if ( p2 != null ) { p2.prevInSet = path; } + size++; + + addCount++; + + // feed random samples to the indices + if ( (addCount & 31) == 0 ) + { + addIndex( path, start ); + } + else if ( (addCount & 1023) == 1023 ) + { + addIndex( path, index2 ); + } + } + + public void remove( OsmPath path ) + { + OsmPath p1 = path.prevInSet; + OsmPath p2 = path.nextInSet; + if ( p1 == null ) + { + return; // not in set + } + path.prevInSet = null; + path.nextInSet = null; + if ( p2 != null ) + { + p2.prevInSet = p1; + } + p1.nextInSet = p2; + + removeIndex( path ); + + size--; + } + + public OsmPath first() + { + return start.nextInSet; + } + + public int size() + { + return size; + } + + public int[] getExtract() + { + int div = size / 1000 + 1; + + int[] res = new int[size/div * 2]; + int i = 0; + int cnt = 0; + for( OsmPath p = start.nextInSet; p != null; p = p.nextInSet ) + { + if ( (++cnt) % div == 0 ) + { + OsmNode n = p.getLink().targetNode; + res[i++] = n.ilon; + res[i++] = n.ilat; + } + } + return res; + } + + // index operations + + private void addIndex( OsmPath path, OsmPath index ) + { + int ac = path.adjustedCost; + OsmPath p1 = index; + OsmPath p2 = p1.nextInIndexSet; + while( p2 != null && p2.adjustedCost < ac ) + { + p1 = p2; + p2 = p2.nextInIndexSet; + } + p1.nextInIndexSet = path; + path.prevInIndexSet = p1; + path.nextInIndexSet = p2; + if ( p2 != null ) { p2.prevInIndexSet = path; } + } + + + private void removeIndex( OsmPath path ) + { + OsmPath p1 = path.prevInIndexSet; + OsmPath p2 = path.nextInIndexSet; + if ( p1 == null ) + { + return; // not in set + } + path.prevInIndexSet = null; + path.nextInIndexSet = null; + if ( p2 != null ) + { + p2.prevInIndexSet = p1; + } + p1.nextInIndexSet = p2; + } + +} diff --git a/brouter-core/src/main/java/btools/router/OsmNodeNamed.java b/brouter-core/src/main/java/btools/router/OsmNodeNamed.java new file mode 100644 index 0000000..1566174 --- /dev/null +++ b/brouter-core/src/main/java/btools/router/OsmNodeNamed.java @@ -0,0 +1,33 @@ +/** + * Container for an osm node + * + * @author ab + */ +package btools.router; + +import btools.mapaccess.OsmNode; + +public class OsmNodeNamed extends OsmNode +{ + public String name; + public double radius; // radius of nogopoint + public boolean isNogo = false; + + @Override + public String toString() + { + return ilon + "," + ilat + "," + name; + } + + public static OsmNodeNamed decodeNogo( String s ) + { + OsmNodeNamed n = new OsmNodeNamed(); + int idx1 = s.indexOf( ',' ); + n.ilon = Integer.parseInt( s.substring( 0, idx1 ) ); + int idx2 = s.indexOf( ',', idx1+1 ); + n.ilat = Integer.parseInt( s.substring( idx1+1, idx2 ) ); + n.name = s.substring( idx2+1 ); + n.isNogo = true; + return n; + } +} diff --git a/brouter-core/src/main/java/btools/router/OsmPath.java b/brouter-core/src/main/java/btools/router/OsmPath.java new file mode 100644 index 0000000..39adac9 --- /dev/null +++ b/brouter-core/src/main/java/btools/router/OsmPath.java @@ -0,0 +1,371 @@ +/** + * Container for link between two Osm nodes + * + * @author ab + */ +package btools.router; + +import btools.mapaccess.*; + +final class OsmPath implements OsmLinkHolder +{ + // double-linked lists for the openSet + public OsmPath nextInSet; + public OsmPath prevInSet; + public OsmPath nextInIndexSet; + public OsmPath prevInIndexSet; + + /** + * The cost of that path (a modified distance) + */ + public int cost = 0; + + /** + * The elevation-hysteresis-buffer (0-10 m) + */ + private int ehbd; // in micrometer + private int ehbu; // in micrometer + + // the elevation assumed for that path can have a value + // if the corresponding node has not + public short selev; + + private static final int MAX_EHB = 10000000; + + public int adjustedCost = 0; + + public void setAirDistanceCostAdjustment( int costAdjustment ) + { + adjustedCost = cost + costAdjustment; + } + + private OsmNode sourcenode; + private OsmLink link; + public OsmPathElement originElement; + + private OsmLinkHolder nextForLink = null; + + public int treedepth = 0; + + // the position of the waypoint just before + // this path position (for angle calculation) + public int originLon; + public int originLat; + + // the costfactor of the segment just before this paths position + public float lastCostfactor; + + public String message; + + OsmPath() + { + } + + OsmPath( OsmLink link ) + { + this(); + this.link = link; + this.selev = link.targetNode.getSElev(); + } + + OsmPath( OsmNode sourcenode, OsmPath origin, OsmLink link, OsmTrack refTrack, boolean recordTransferNodes, RoutingContext rc ) + { + this(); + this.originElement = new OsmPathElement( origin ); + this.link = link; + this.sourcenode = sourcenode; + this.cost = origin.cost; + this.ehbd = origin.ehbd; + this.ehbu = origin.ehbu; + this.lastCostfactor = origin.lastCostfactor; + addAddionalPenalty(refTrack, recordTransferNodes, origin, link, rc ); + } + + private void addAddionalPenalty(OsmTrack refTrack, boolean recordTransferNodes, OsmPath origin, OsmLink link, RoutingContext rc ) + { + rc.nogomatch = false; + + // extract the 3 positions of the first section + int lon0 = origin.originLon; + int lat0 = origin.originLat; + + OsmNode p1 = origin.link.targetNode; + int lon1 = p1.getILon(); + int lat1 = p1.getILat(); + short ele1 = origin.selev; + + int linkdisttotal = 0; + int linkdist = 0; + int linkelevationcost = 0; + int linkturncost = 0; + + OsmTransferNode transferNode = link.decodeFirsttransfer(); + OsmNode targetNode = link.targetNode; + long lastDescription = -1L; + String lastMessage = null; + for(;;) + { + originLon = lon1; + originLat = lat1; + + int lon2; + int lat2; + short ele2; + long description; + + if ( transferNode == null ) + { + lon2 = targetNode.ilon; + lat2 = targetNode.ilat; + ele2 = targetNode.selev; + description = link.descriptionBitmap; + } + else + { + lon2 = transferNode.ilon; + lat2 = transferNode.ilat; + ele2 = transferNode.selev; + description = transferNode.descriptionBitmap; + } + + // if way description changed, store message + if ( lastMessage != null && description != lastDescription ) + { + originElement.message = lastMessage; + linkdist = 0; + linkelevationcost = 0; + linkturncost = 0; + } + lastDescription = description; + + int dist = rc.calcDistance( lon1, lat1, lon2, lat2 ); + int elefactor = 250000; + boolean stopAtEndpoint = false; + if ( rc.shortestmatch ) + { + elefactor = (int)(elefactor*rc.wayfraction); + + if ( rc.isEndpoint ) + { + stopAtEndpoint = true; + } + else + { + // we just start here, reset cost + cost = 0; + ehbd = 0; + ehbu = 0; + if ( recordTransferNodes ) + { + if ( rc.wayfraction > 0. ) + { + originElement = new OsmPathElement( rc.ilonshortest, rc.ilatshortest, ele2, null ); + } + else + { + originElement = null; // prevent duplicate point + } + } + } + } + + linkdist += dist; + linkdisttotal += dist; + + rc.messageHandler.setCurrentPos( lon2, lat2 ); + rc.expctxWay.evaluate( description, rc.messageHandler ); + + // *** penalty for way-change + if ( origin.originElement != null ) + { + // penalty proportional to direction change + double cos = rc.calcCosAngle( lon0, lat0, lon1, lat1, lon2, lat2 ); + int turncost = (int)(cos * rc.expctxWay.getTurncost() + 0.2 ); // e.g. turncost=90 -> 90 degree = 90m penalty + cost += turncost; + linkturncost += turncost; + } + + // *** penalty for elevation (penalty is for descend! in a way that slow descends give no penalty) + // only the part of the descend that does not fit into the elevation-hysteresis-buffer + // leads to an immediate penalty + + if ( ele2 == Short.MIN_VALUE ) ele2 = ele1; + if ( ele1 != Short.MIN_VALUE ) + { + ehbd += (ele1 - ele2)*elefactor - dist * rc.downhillcutoff; + ehbu += (ele2 - ele1)*elefactor - dist * rc.uphillcutoff; + } + + if ( ehbd > MAX_EHB ) + { + if ( rc.downhillcostdiv > 0 ) + { + int elevationCost = (ehbd-MAX_EHB)/rc.downhillcostdiv; + cost += elevationCost; + linkelevationcost += elevationCost; + } + ehbd = MAX_EHB; + } + else if ( ehbd < 0 ) + { + ehbd = 0; + } + + if ( ehbu > MAX_EHB ) + { + if ( rc.uphillcostdiv > 0 ) + { + int elevationCost = (ehbu-MAX_EHB)/rc.uphillcostdiv; + cost += elevationCost; + linkelevationcost += elevationCost; + } + ehbu = MAX_EHB; + } + else if ( ehbu < 0 ) + { + ehbu = 0; + } + + // *** penalty for distance + float costfactor = rc.expctxWay.getCostfactor(); + float fcost = dist * costfactor + 0.5f; + if ( costfactor >= 10000. || fcost + cost >= 2000000000. ) + { + cost = -1; + return; + } + int waycost = (int)(fcost); + cost += waycost; + + // *** add initial cost if factor changed + float costdiff = costfactor - lastCostfactor; + if ( costdiff > 0.0005 || costdiff < -0.0005 ) + { + lastCostfactor = costfactor; + float initialcost = rc.expctxWay.getInitialcost(); + int iicost = (int)initialcost; + cost += iicost; + } + + if ( recordTransferNodes ) + { + int iCost = (int)(rc.expctxWay.getCostfactor()*1000 + 0.5f); + lastMessage = (lon2-180000000) + "\t" + + (lat2-90000000) + "\t" + + ele2/4 + "\t" + + linkdist + "\t" + + iCost + "\t" + + linkelevationcost + + "\t" + linkturncost + + rc.expctxWay.getCsvDescription( description ); + } + + if ( stopAtEndpoint ) + { + if ( recordTransferNodes ) + { + originElement = new OsmPathElement( rc.ilonshortest, rc.ilatshortest, ele2, originElement ); + originElement.cost = cost; + } + if ( rc.nogomatch ) + { + cost = -1; + } + return; + } + + if ( transferNode == null ) + { + // *** penalty for being part of the reference track + if ( refTrack != null && refTrack.containsNode( targetNode ) && refTrack.containsNode( origin.link.targetNode ) ) + { + int reftrackcost = linkdisttotal; + cost += reftrackcost; + } + message = lastMessage; + selev = ele2; + break; + } + transferNode = transferNode.next; + + if ( recordTransferNodes ) + { + originElement = new OsmPathElement( lon2, lat2, ele2, originElement ); + originElement.cost = cost; + } + lon0 = lon1; + lat0 = lat1; + lon1 = lon2; + lat1 = lat2; + ele1 = ele2; + + } + + // check for nogo-matches (after the *actual* start of segment) + if ( rc.nogomatch ) + { + cost = -1; + return; + } + + // finally add node-costs for target node + if ( targetNode.nodeDescription != 0L ) + { + rc.messageHandler.setCurrentPos( targetNode.ilon, targetNode.ilat ); + rc.expctxNode.evaluate( targetNode.nodeDescription, rc.messageHandler ); + float initialcost = rc.expctxNode.getInitialcost(); + if ( initialcost >= 1000000. ) + { + cost = -1; + return; + } + int iicost = (int)initialcost; + cost += iicost; + } + } + + public int elevationCorrection( RoutingContext rc ) + { + return ( rc.downhillcostdiv > 0 ? ehbd/rc.downhillcostdiv : 0 ) + + ( rc.uphillcostdiv > 0 ? ehbu/rc.uphillcostdiv : 0 ); + } + + public boolean definitlyWorseThan( OsmPath p, RoutingContext rc ) + { + int c = p.cost; + if ( rc.downhillcostdiv > 0 ) + { + int delta = p.ehbd - ehbd; + if ( delta > 0 ) c += delta/rc.downhillcostdiv; + } + if ( rc.uphillcostdiv > 0 ) + { + int delta = p.ehbu - ehbu; + if ( delta > 0 ) c += delta/rc.uphillcostdiv; + } + + return cost > c; + } + + + public OsmNode getSourceNode() + { + return sourcenode; + } + + public OsmLink getLink() + { + return link; + } + + + public void setNextForLink( OsmLinkHolder holder ) + { + nextForLink = holder; + } + + public OsmLinkHolder getNextForLink() + { + return nextForLink; + } +} diff --git a/brouter-core/src/main/java/btools/router/OsmPathElement.java b/brouter-core/src/main/java/btools/router/OsmPathElement.java new file mode 100644 index 0000000..af89341 --- /dev/null +++ b/brouter-core/src/main/java/btools/router/OsmPathElement.java @@ -0,0 +1,113 @@ +/** + * Container for link between two Osm nodes + * + * @author ab + */ +package btools.router; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +import btools.mapaccess.OsmNode; +import btools.mapaccess.OsmPos; + +final class OsmPathElement implements OsmPos +{ + private int ilat; // latitude + private int ilon; // longitude + private short selev; // longitude + + public String message = null; // description + + public int cost; + + // interface OsmPos + public int getILat() + { + return ilat; + } + + public int getILon() + { + return ilon; + } + + public short getSElev() + { + return selev; + } + + public double getElev() + { + return selev / 4.; + } + + public long getIdFromPos() + { + return ((long)ilon)<<32 | ilat; + } + + public int calcDistance( OsmPos p ) + { + double l = (ilat-90000000) * 0.00000001234134; + double l2 = l*l; + double l4 = l2*l2; + double coslat = 1.- l2 + l4 / 6.; + + double dlat = (ilat - p.getILat() )/1000000.; + double dlon = (ilon - p.getILon() )/1000000. * coslat; + double d = Math.sqrt( dlat*dlat + dlon*dlon ) * (6378000. / 57.); + return (int)(d + 1.0 ); + } + + public OsmPathElement origin; + + // construct a path element from a path + public OsmPathElement( OsmPath path ) + { + OsmNode n = path.getLink().targetNode; + ilat = n.getILat(); + ilon = n.getILon(); + selev = path.selev; + cost = path.cost; + + origin = path.originElement; + message = path.message; + } + + public OsmPathElement( int ilon, int ilat, short selev, OsmPathElement origin ) + { + this.ilon = ilon; + this.ilat = ilat; + this.selev = selev; + this.origin = origin; + } + + private OsmPathElement() + { + } + + public String toString() + { + return ilon + "_" + ilat; + } + + public void writeToStream( DataOutput dos ) throws IOException + { + dos.writeInt( ilat ); + dos.writeInt( ilon ); + dos.writeShort( selev ); + dos.writeInt( cost ); + } + + public static OsmPathElement readFromStream( DataInput dis ) throws IOException + { + OsmPathElement pe = new OsmPathElement(); + pe.ilat = dis.readInt(); + pe.ilon = dis.readInt(); + pe.selev = dis.readShort(); + pe.cost = dis.readInt(); + return pe; + } +} diff --git a/brouter-core/src/main/java/btools/router/OsmTrack.java b/brouter-core/src/main/java/btools/router/OsmTrack.java new file mode 100644 index 0000000..2f2a485 --- /dev/null +++ b/brouter-core/src/main/java/btools/router/OsmTrack.java @@ -0,0 +1,365 @@ +/** + * Container for a track + * + * @author ab + */ +package btools.router; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.InputStreamReader; +import java.util.ArrayList; + +import btools.mapaccess.OsmPos; +import btools.util.CompactLongMap; +import btools.util.FrozenLongMap; + +public final class OsmTrack +{ + public MatchedWaypoint endPoint; + + private class OsmPathElementHolder + { + public OsmPathElement node; + public OsmPathElementHolder nextHolder; + } + + + public ArrayList nodes = new ArrayList(); + + private CompactLongMap nodesMap; + + public String message = null; + public ArrayList messageList = null; + + public String name = "unset"; + + public void addNode( OsmPathElement node ) + { + nodes.add( 0, node ); + } + + public void buildMap() + { + nodesMap = new CompactLongMap(); + for( OsmPathElement node: nodes ) + { + long id = node.getIdFromPos(); + OsmPathElementHolder nh = new OsmPathElementHolder(); + nh.node = node; + OsmPathElementHolder h = nodesMap.get( id ); + if ( h != null ) + { + while( h.nextHolder != null ) + { + h = h.nextHolder; + } + h.nextHolder = nh; + } + else + { + nodesMap.fastPut( id, nh ); + } + } + nodesMap = new FrozenLongMap( nodesMap ); + } + + /** + * writes the track in binary-format to a file + * @param filename the filename to write to + */ + public void writeBinary( String filename ) throws Exception + { + DataOutputStream dos = new DataOutputStream( new BufferedOutputStream( new FileOutputStream( filename ) ) ); + + endPoint.writeToStream( dos ); + dos.writeInt( nodes.size() ); + for( OsmPathElement node: nodes ) + { + node.writeToStream( dos ); + } + dos.close(); + } + + public static OsmTrack readBinary( String filename, OsmNodeNamed newEp ) + { + OsmTrack t = null; + if ( filename != null ) + { + File f = new File( filename ); + if ( f.exists() ) + { + try + { + DataInputStream dis = new DataInputStream( new BufferedInputStream( new FileInputStream( f ) ) ); + MatchedWaypoint ep = MatchedWaypoint.readFromStream( dis ); + int dlon = ep.waypoint.ilon - newEp.ilon; + int dlat = ep.waypoint.ilat - newEp.ilat; + if ( dlon < 20 && dlon > -20 && dlat < 20 && dlat > -20 ) + { + t = new OsmTrack(); + t.endPoint = ep; + int n = dis.readInt(); + OsmPathElement last_pe = null; + for( int i=0; i 0 || nodes.size() == 0 ) + { + nodes.add( t.nodes.get(i) ); + } + } + distance += t.distance; + ascend += t.ascend; + plainAscend += t.plainAscend; + cost += t.cost; + } + + public int distance; + public int ascend; + public int plainAscend; + public int cost; + + /** + * writes the track in gpx-format to a file + * @param filename the filename to write to + */ + public void writeGpx( String filename ) throws Exception + { + BufferedWriter bw = new BufferedWriter( new FileWriter( filename ) ); + + bw.write( formatAsGpx() ); + bw.close(); + } + + public String formatAsGpx() + { + StringBuilder sb = new StringBuilder(8192); + + sb.append( "\n" ); + for( int i=messageList.size()-1; i >= 0; i-- ) + { + String message = messageList.get(i); + if ( i < messageList.size()-1 ) message = "(alt-index " + i + ": " + message + " )"; + if ( message != null ) sb.append( "\n" ); + } + sb.append( "\n" ); + sb.append( " \n" ); + sb.append( " " + name + "\n" ); + sb.append( " \n" ); + + for( OsmPathElement n : nodes ) + { + String sele = n.getSElev() == Short.MIN_VALUE ? "" : "" + n.getElev() + ""; + sb.append( " " + sele + "\n" ); + } + + sb.append( " \n" ); + sb.append( " \n" ); + sb.append( "\n" ); + + return sb.toString(); + } + + public void writeKml( String filename ) throws Exception + { + BufferedWriter bw = new BufferedWriter( new FileWriter( filename ) ); + + bw.write( formatAsKml() ); + bw.close(); + } + + public String formatAsKml() + { + StringBuilder sb = new StringBuilder(8192); + + sb.append( "\n" ); + + sb.append( "\n" ); + sb.append( " \n" ); + sb.append( " KML Samples\n" ); + sb.append( " 1\n" ); + sb.append( " 3.497064\n" ); + sb.append( " 872\n" ); + sb.append( " To enable simple instructions add: 'instructions=1' as parameter to the URL\n" ); + sb.append( " \n" ); + sb.append( " Paths\n" ); + sb.append( " 0\n" ); + sb.append( " Examples of paths.\n" ); + sb.append( " \n" ); + sb.append( " Tessellated\n" ); + sb.append( " 0\n" ); + sb.append( " tag has a value of 1, the line will contour to the underlying terrain]]>\n" ); + sb.append( " \n" ); + sb.append( " 1\n" ); + sb.append( " " ); + + + for( OsmPathElement n : nodes ) + { + sb.append( formatPos( n.getILon() - 180000000 ) + "," + formatPos( n.getILat() - 90000000 ) + "\n" ); + } + + sb.append( " \n" ); + sb.append( " \n" ); + sb.append( " \n" ); + sb.append( " \n" ); + sb.append( " \n" ); + sb.append( "\n" ); + + return sb.toString(); + } + + private static String formatPos( int p ) + { + boolean negative = p < 0; + if ( negative ) p = -p; + char[] ac = new char[12]; + int i = 11; + while( p != 0 || i > 3 ) + { + ac[i--] = (char)('0' + (p % 10)); + p /= 10; + if ( i == 5 ) ac[i--] = '.'; + } + if ( negative ) ac[i--] = '-'; + return new String( ac, i+1, 11-i ); + } + + public void dumpMessages( String filename, RoutingContext rc ) throws Exception + { + BufferedWriter bw = filename == null ? null : new BufferedWriter( new FileWriter( filename ) ); + + // csv-header-line + + String header = "Longitude\tLatitude\tElevation\tDistance\tCostPerKm\tElevCost\tTurnCost"; + if ( rc.expctxWay != null ) + { + header += rc.expctxWay.getCsvHeader(); + } + dumpLine( bw, header ); + for( OsmPathElement n : nodes ) + { + if ( n.message != null ) + { + dumpLine( bw, n.message ); + } + } + if ( bw != null ) bw.close(); + } + + private void dumpLine( BufferedWriter bw, String s) throws Exception + { + if ( bw == null ) + { + System.out.println( s ); + } + else + { + bw.write( s ); + bw.write( "\n" ); + } + } + + public void readGpx( String filename ) throws Exception + { + File f = new File( filename ); + if ( !f.exists() ) return; + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream( f ) ) ); + + for(;;) + { + String line = br.readLine(); + if ( line == null ) break; + + int idx0 = line.indexOf( "= 0 ) + { + idx0 += 12; + int idx1 = line.indexOf( '"', idx0 ); + int ilon = (int)((Double.parseDouble( line.substring( idx0, idx1 ) ) + 180. )*1000000. + 0.5); + int idx2 = line.indexOf( " lat=\"" ); + if ( idx2 < 0 ) continue; + idx2 += 6; + int idx3 = line.indexOf( '"', idx2 ); + int ilat = (int)((Double.parseDouble( line.substring( idx2, idx3 ) ) + 90. )*1000000. + 0.5); + nodes.add( new OsmPathElement( ilon, ilat, (short)0, null ) ); + } + } + br.close(); + } + + public boolean equalsTrack( OsmTrack t ) + { + if ( nodes.size() != t.nodes.size() ) return false; + for( int i=0; i 3 ) idx = 3; + alternativeIdx = idx; + } + public int getAlternativeIdx() + { + return alternativeIdx; + } + public int alternativeIdx = 0; + public String localFunction; + + public String rawTrackPath; + + public String getProfileName() + { + String name = localFunction == null ? "unknown" : localFunction; + if ( name.endsWith( ".brf" ) ) name = name.substring( 0, localFunction.length() - 4 ); + int idx = name.lastIndexOf( '/' ); + if ( idx >= 0 ) name = name.substring( idx+1 ); + return name; + } + + public BExpressionContext expctxWay; + public BExpressionContext expctxNode; + + public int downhillcostdiv; + public int downhillcutoff; + public int uphillcostdiv; + public int uphillcutoff; + public boolean carMode; + public double pass1coefficient; + public double pass2coefficient; + + public void readGlobalConfig( BExpressionContext expctxGlobal ) + { + downhillcostdiv = (int)expctxGlobal.getVariableValue( "downhillcost" ); + downhillcutoff = (int)(expctxGlobal.getVariableValue( "downhillcutoff" )*10000); + uphillcostdiv = (int)expctxGlobal.getVariableValue( "uphillcost" ); + uphillcutoff = (int)(expctxGlobal.getVariableValue( "uphillcutoff" )*10000); + if ( downhillcostdiv != 0 ) downhillcostdiv = 1000000/downhillcostdiv; + if ( uphillcostdiv != 0 ) uphillcostdiv = 1000000/uphillcostdiv; + carMode = 0.f != expctxGlobal.getVariableValue( "validForCars" ); + pass1coefficient = expctxGlobal.getVariableValue( "pass1coefficient", 1.5f ); + pass2coefficient = expctxGlobal.getVariableValue( "pass2coefficient", 0.f ); + } + + public RoutingMessageHandler messageHandler = new RoutingMessageHandler(); + + public List nogopoints = null; + private List keepnogopoints = null; + + private double coslat; + public boolean nogomatch = false; + public boolean isEndpoint = false; + + public boolean shortestmatch = false; + public double wayfraction; + public int ilatshortest; + public int ilonshortest; + + public void prepareNogoPoints( List nogos ) + { + for( OsmNodeNamed nogo : nogos ) + { + String s = nogo.name; + int idx = s.indexOf( ' ' ); + if ( idx > 0 ) s = s.substring( 0 , idx ); + int ir = 20; // default radius + if ( s.length() > 4 ) + { + try { ir = Integer.parseInt( s.substring( 4 ) ); } + catch( Exception e ) { /* ignore */ } + } + nogo.radius = ir / 111894.; // 6378000. / 57.; + } + } + + public void setWaypoint( OsmNodeNamed wp, boolean endpoint ) + { + keepnogopoints = nogopoints; + nogopoints = new ArrayList(); + nogopoints.add( wp ); + if ( keepnogopoints != null ) nogopoints.addAll( keepnogopoints ); + isEndpoint = endpoint; + } + + public void unsetWaypoint() + { + nogopoints = keepnogopoints; + isEndpoint = false; + } + + public int calcDistance( int lon1, int lat1, int lon2, int lat2 ) + { + double l = (lat2 - 90000000) * 0.00000001234134; + double l2 = l*l; + double l4 = l2*l2; + coslat = 1.- l2 + l4 / 6.; + double coslat6 = coslat*0.000001; + + double dx = (lon2 - lon1 ) * coslat6; + double dy = (lat2 - lat1 ) * 0.000001; + double d = Math.sqrt( dy*dy + dx*dx ); + + shortestmatch = false; + + if ( d > 0. && nogopoints != null ) + { + for( OsmNodeNamed nogo : nogopoints ) + { + double x1 = (lon1 - nogo.ilon) * coslat6; + double y1 = (lat1 - nogo.ilat) * 0.000001; + double x2 = (lon2 - nogo.ilon) * coslat6; + double y2 = (lat2 - nogo.ilat) * 0.000001; + double r12 = x1*x1 + y1*y1; + double r22 = x2*x2 + y2*y2; + double radius = Math.abs( r12 < r22 ? y1*dx - x1*dy : y2*dx - x2*dy ) / d; + + if ( radius < nogo.radius ) // 20m + { + double s1 = x1*dx + y1*dy; + double s2 = x2*dx + y2*dy; + + + if ( s1 < 0. ) { s1 = -s1; s2 = -s2; } + if ( s2 > 0. ) + { + radius = Math.sqrt( s1 < s2 ? r12 : r22 ); + if ( radius > nogo.radius ) continue; // 20m ^ 2 + } + if ( nogo.isNogo ) nogomatch = true; + else + { + shortestmatch = true; + nogo.radius = radius; // shortest distance to way + // calculate remaining distance + if ( s2 < 0. ) + { + double distance = d > 0. ? -s2 / d : 0.; + wayfraction = d > 0. ? distance / d : 0.; + double xm = x2 - wayfraction*dx; + double ym = y2 - wayfraction*dy; + ilonshortest = (int)(xm / coslat6 + nogo.ilon); + ilatshortest = (int)(ym / 0.000001 + nogo.ilat); + } + else if ( s1 > s2 ) + { + wayfraction = 0.; + ilonshortest = lon2; + ilatshortest = lat2; + } + else + { + wayfraction = 1.; + ilonshortest = lon1; + ilatshortest = lat1; + } + + // here it gets nasty: there can be nogo-points in the list + // *after* the shortest distance point. In case of a shortest-match + // we use the reduced way segment for nogo-matching, in order not + // to cut our escape-way if we placed a nogo just in front of where we are + if ( isEndpoint ) + { + wayfraction = 1. - wayfraction; + lon2 = ilonshortest; + lat2 = ilatshortest; + } + else + { + nogomatch = false; + lon1 = ilonshortest; + lat1 = ilatshortest; + } + dx = (lon2 - lon1 ) * coslat6; + dy = (lat2 - lat1 ) * 0.000001; + d = Math.sqrt( dy*dy + dx*dx ); + } + } + } + } + double dd = d * 111894.7368; // 6378000. / 57.; + return (int)(dd + 1.0 ); + } + + // assumes that calcDistance/calcCosAngle called in sequence, so coslat valid + public double calcCosAngle( int lon0, int lat0, int lon1, int lat1, int lon2, int lat2 ) + { + double dlat1 = (lat1 - lat0); + double dlon1 = (lon1 - lon0) * coslat; + double dlat2 = (lat2 - lat1); + double dlon2 = (lon2 - lon1) * coslat; + + double dd = Math.sqrt( (dlat1*dlat1 + dlon1*dlon1)*(dlat2*dlat2 + dlon2*dlon2) ); + if ( dd == 0. ) return 0.; + double cosp = (dlat1*dlat2 + dlon1*dlon2)/dd; + return 1.-cosp; // don't care to really do acos.. + } + + @Override + public boolean isWithinRadius( int ilon0, int ilat0, OsmTransferNode firstTransfer, int ilon1, int ilat1 ) + { + OsmNodeNamed wp = nogopoints.get(0); + double keepRadius = wp.radius; + try + { + int ilon = ilon0; + int ilat = ilat0; + for( OsmTransferNode trans = firstTransfer; trans != null; trans = trans.next ) + { + calcDistance( ilon, ilat, trans.ilon, trans.ilat ); + ilon = trans.ilon; + ilat = trans.ilat; + } + calcDistance( ilon, ilat, ilon1, ilat1 ); + return wp.radius < keepRadius; + } + finally + { + wp.radius = keepRadius; + } + } + +} diff --git a/brouter-core/src/main/java/btools/router/RoutingEngine.java b/brouter-core/src/main/java/btools/router/RoutingEngine.java new file mode 100644 index 0000000..ad276bc --- /dev/null +++ b/brouter-core/src/main/java/btools/router/RoutingEngine.java @@ -0,0 +1,968 @@ +package btools.router; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import btools.expressions.BExpressionContext; +import btools.mapaccess.NodesCache; +import btools.mapaccess.OsmLink; +import btools.mapaccess.OsmLinkHolder; +import btools.mapaccess.OsmNode; +import btools.mapaccess.OsmNodesMap; + +public class RoutingEngine extends Thread +{ + private OsmNodesMap nodesMap; + private NodesCache nodesCache; + private OpenSet openSet = new OpenSet(); + private boolean finished = false; + + private List waypoints = null; + private int linksProcessed = 0; + + private OsmTrack foundTrack = new OsmTrack(); + private OsmTrack foundRawTrack = null; + private int alternativeIndex = 0; + + private String errorMessage = null; + + private volatile boolean terminated; + + private String segmentDir; + private String outfileBase; + private String logfileBase; + private boolean infoLogEnabled; + private RoutingContext routingContext; + + private double airDistanceCostFactor; + private OsmTrack guideTrack; + + private OsmPathElement matchPath; + + private long startTime; + private long maxRunningTime; + + public boolean quite = false; + + public RoutingEngine( String outfileBase, String logfileBase, String segmentDir, + List waypoints, RoutingContext rc ) + { + this.segmentDir = segmentDir; + this.outfileBase = outfileBase; + this.logfileBase = logfileBase; + this.waypoints = waypoints; + this.infoLogEnabled = outfileBase != null; + this.routingContext = rc; + + if ( rc.localFunction != null ) + { + String profileBaseDir = System.getProperty( "profileBaseDir" ); + File profileDir; + File profileFile; + if ( profileBaseDir == null ) + { + profileDir = new File( rc.localFunction ).getParentFile(); + profileFile = new File( rc.localFunction ) ; + } + else + { + profileDir = new File( profileBaseDir ); + profileFile = new File( profileDir, rc.localFunction + ".brf" ) ; + } + BExpressionContext expctxGlobal = new BExpressionContext( "global" ); + expctxGlobal.readMetaData( new File( profileDir, "lookups.dat" ) ); + expctxGlobal.parseFile( profileFile, null ); + expctxGlobal.evaluate( 1L, rc.messageHandler ); + rc.readGlobalConfig(expctxGlobal); + + rc.expctxWay = new BExpressionContext( "way", 4096 ); + rc.expctxWay.readMetaData( new File( profileDir, "lookups.dat" ) ); + rc.expctxWay.parseFile( profileFile, "global" ); + + rc.expctxNode = new BExpressionContext( "node", 1024 ); + rc.expctxNode.readMetaData( new File( profileDir, "lookups.dat" ) ); + rc.expctxNode.parseFile( profileFile, "global" ); + } + } + + private void logInfo( String s ) + { + if ( infoLogEnabled ) + { + System.out.println( s ); + } + } + + public void run() + { + doRun( 0 ); + } + + public void doRun( long maxRunningTime ) + { + try + { + startTime = System.currentTimeMillis(); + this.maxRunningTime = maxRunningTime; + OsmTrack sum = null; + OsmTrack track = null; + ArrayList messageList = new ArrayList(); + for( int i=0; !terminated; i++ ) + { + track = findTrack( sum ); + track.message = "track-length = " + track.distance + " filtered ascend = " + track.ascend + + " plain-ascend = " + track.plainAscend + " cost=" + track.cost; + track.name = "brouter_" + routingContext.getProfileName() + "_" + i; + + messageList.add( track.message ); + track.messageList = messageList; + if ( outfileBase != null ) + { + String filename = outfileBase + i + ".gpx"; + OsmTrack oldTrack = new OsmTrack(); + oldTrack.readGpx(filename); + if ( track.equalsTrack( oldTrack ) ) + { + if ( sum == null ) sum = new OsmTrack(); + sum.addNodes( track ); + continue; + } + track.writeGpx( filename ); + foundTrack = track; + alternativeIndex = i; + } + else + { + if ( i == routingContext.getAlternativeIdx() ) + { + if ( "CSV".equals( System.getProperty( "reportFormat" ) ) ) + { + track.dumpMessages( null, routingContext ); + } + else + { + if ( !quite ) + { + System.out.println( track.formatAsGpx() ); + } + } + foundTrack = track; + } + else + { + if ( sum == null ) sum = new OsmTrack(); + sum.addNodes( track ); + continue; + } + } + if ( logfileBase != null ) + { + String logfilename = logfileBase + i + ".csv"; + track.dumpMessages( logfilename, routingContext ); + } + break; + } + long endTime = System.currentTimeMillis(); + logInfo( "execution time = " + (endTime-startTime)/1000. + " seconds" ); + } + catch( Exception e) + { + errorMessage = e instanceof IllegalArgumentException ? e.getMessage() : e.toString(); + logInfo( "Exception (linksProcessed=" + linksProcessed + ": " + errorMessage ); + e.printStackTrace(); + } + catch( Error e) + { + String hint = cleanOnOOM(); + errorMessage = e.toString() + hint; + logInfo( "Error (linksProcessed=" + linksProcessed + ": " + errorMessage ); + e.printStackTrace(); + } + finally + { + openSet.clear(); + finished = true; // this signals termination to outside + } + } + + public String cleanOnOOM() + { + boolean oom_carsubset_hint = nodesCache == null ? false : nodesCache.oom_carsubset_hint; + nodesMap = null; + nodesCache = null; + terminate(); + return oom_carsubset_hint ? "\nPlease use 'carsubset' maps for long-distance car-routing" : ""; + } + + + + private OsmTrack findTrack( OsmTrack refTrack ) + { + OsmTrack totaltrack = new OsmTrack(); + MatchedWaypoint[] wayointIds = new MatchedWaypoint[waypoints.size()]; + + // check for a track for that target + OsmTrack nearbyTrack = null; + if ( refTrack == null ) + { + nearbyTrack = OsmTrack.readBinary( routingContext.rawTrackPath, waypoints.get( waypoints.size()-1) ); + if ( nearbyTrack != null ) + { + wayointIds[waypoints.size()-1] = nearbyTrack.endPoint; + } + } + + // match waypoints to nodes + for( int i=0; i nodeList = nodesCache.getAllNodes(); + + MatchedWaypoint mwp = new MatchedWaypoint(); + mwp.waypoint = wp; + + // first loop just to expand reverse links + for( OsmNode n : nodeList ) + { + if ( !nodesCache.obtainNonHollowNode( n ) ) + { + continue; + } + expandHollowLinkTargets( n, false ); + OsmLink startLink = new OsmLink(); + startLink.targetNode = n; + OsmPath startPath = new OsmPath( startLink ); + startLink.addLinkHolder( startPath ); + for( OsmLink link = n.firstlink; link != null; link = link.next ) + { + if ( link.counterLinkWritten ) continue; // reverse link not found + OsmNode nextNode = link.targetNode; + if ( nextNode.isHollow() ) continue; // border node? + if ( nextNode.firstlink == null ) continue; // don't care about dead ends + if ( nextNode == n ) continue; // ? + double oldRadius = wp.radius; + OsmPath testPath = new OsmPath( n, startPath, link, null, false, routingContext ); + if ( wp.radius < oldRadius ) + { + if ( testPath.cost < 0 ) + { + wp.radius = oldRadius; // no valid way + } + else + { + mwp.node1 = n; + mwp.node2 = nextNode; + mwp.radius = wp.radius; + mwp.cost = testPath.cost; + mwp.crosspoint = new OsmNodeNamed(); + mwp.crosspoint.ilon = routingContext.ilonshortest; + mwp.crosspoint.ilat = routingContext.ilatshortest; + } + } + } + } + return mwp; + } + + // expand hollow link targets and resolve reverse links + private void expandHollowLinkTargets( OsmNode n, boolean failOnReverseNotFound ) + { + for( OsmLink link = n.firstlink; link != null; link = link.next ) + { + if ( ! nodesCache.obtainNonHollowNode( link.targetNode ) ) + { + continue; + } + + if ( link.counterLinkWritten ) + { + OsmLink rlink = link.targetNode.getReverseLink( n.getILon(), n.getILat() ); + if ( rlink == null ) + { + if ( failOnReverseNotFound ) throw new RuntimeException( "reverse link not found!" ); + } + else + { + link.descriptionBitmap = rlink.descriptionBitmap; + link.firsttransferBytes = rlink.firsttransferBytes; + link.counterLinkWritten = false; + } + } + } + n.wasProcessed = true; + } + + private OsmTrack searchTrack( MatchedWaypoint startWp, MatchedWaypoint endWp, OsmTrack nearbyTrack, OsmTrack refTrack ) + { + OsmTrack track = null; + double[] airDistanceCostFactors = new double[]{ routingContext.pass1coefficient, routingContext.pass2coefficient }; + boolean isDirty = false; + + if ( nearbyTrack != null ) + { + airDistanceCostFactor = 0.; + try + { + track = findTrack( "re-routing", startWp, endWp, nearbyTrack , refTrack, true ); + } + catch( IllegalArgumentException iae ) + { + // fast partial recalcs: if that timed out, but we had a match, + // build the concatenation from the partial and the nearby track + if ( matchPath != null ) + { + track = mergeTrack( matchPath, nearbyTrack ); + isDirty = true; + } + maxRunningTime += System.currentTimeMillis() - startTime; // reset timeout... + } + } + + if ( track == null ) + { + for( int cfi = 0; cfi < airDistanceCostFactors.length && !terminated; cfi++ ) + { + airDistanceCostFactor = airDistanceCostFactors[cfi]; + + if ( airDistanceCostFactor < 0. ) + { + continue; + } + + OsmTrack t = findTrack( cfi == 0 ? "pass0" : "pass1", startWp, endWp, track , refTrack, false ); + if ( t == null && track != null && matchPath != null ) + { + // ups, didn't find it, use a merge + t = mergeTrack( matchPath, track ); + } + if ( t != null ) + { + track = t; + } + else + { + throw new IllegalArgumentException( "no track found at pass=" + cfi ); + } + } + } + if ( track == null ) throw new IllegalArgumentException( "no track found" ); + + if ( refTrack == null && !isDirty ) + { + track.endPoint = endWp; + foundRawTrack = track; + } + + // final run for verbose log info and detail nodes + airDistanceCostFactor = 0.; + guideTrack = track; + try + { + OsmTrack tt = findTrack( "re-tracking", startWp, endWp, null , refTrack, false ); + if ( tt == null ) throw new IllegalArgumentException( "error re-tracking track" ); + return tt; + } + finally + { + guideTrack = null; + } + } + + + private void resetCache() + { + nodesMap = new OsmNodesMap(); + nodesCache = new NodesCache(segmentDir, nodesMap, routingContext.expctxWay.lookupVersion, routingContext.carMode, nodesCache ); + } + + private OsmNode getStartNode( long startId ) + { + // initialize the start-node + OsmNode start = nodesMap.get( startId ); + if ( start == null ) + { + start = new OsmNode( startId ); + start.setHollow(); + nodesMap.put( startId, start ); + } + if ( !nodesCache.obtainNonHollowNode( start ) ) + { + return null; + } + expandHollowLinkTargets( start, true ); + return start; + } + + private OsmPath getStartPath( OsmNode n1, OsmNode n2, MatchedWaypoint mwp, MatchedWaypoint endWp, boolean sameSegmentSearch ) + { + OsmPath p = getStartPath( n1, n2, mwp.waypoint, endWp.crosspoint ); + + // special case: start+end on same segment + if ( sameSegmentSearch ) + { + OsmPath pe = getEndPath( n1, p.getLink(), endWp.crosspoint, endWp.crosspoint ); + OsmPath pt = getEndPath( n1, p.getLink(), null, endWp.crosspoint ); + int costdelta = pt.cost - p.cost; + if ( pe.cost >= costdelta ) + { + pe.cost -= costdelta; + pe.adjustedCost -= costdelta; + + if ( guideTrack != null ) + { + // nasty stuff: combine the path cause "new OsmPath()" cannot handle start+endpoint + OsmPathElement startElement = p.originElement; + while( startElement.origin != null ) + { + startElement = startElement.origin; + } + if ( pe.originElement.cost > costdelta ) + { + OsmPathElement e = pe.originElement; + while( e.origin != null && e.origin.cost > costdelta ) + { + e = e.origin; + e.cost -= costdelta; + } + e.origin = startElement; + } + else + { + pe.originElement = startElement; + } + } + return pe; + } + } + return p; + } + + + + private OsmPath getStartPath( OsmNode n1, OsmNode n2, OsmNodeNamed wp, OsmNode endPos ) + { + try + { + routingContext.setWaypoint( wp, false ); + OsmPath bestPath = null; + OsmLink bestLink = null; + OsmLink startLink = new OsmLink(); + startLink.targetNode = n1; + OsmPath startPath = new OsmPath( startLink ); + startLink.addLinkHolder( startPath ); + double minradius = 1e10; + for( OsmLink link = n1.firstlink; link != null; link = link.next ) + { + OsmNode nextNode = link.targetNode; + if ( nextNode.isHollow() ) continue; // border node? + if ( nextNode.firstlink == null ) continue; // don't care about dead ends + if ( nextNode == n1 ) continue; // ? + if ( nextNode != n2 ) continue; // just that link + + wp.radius = 1e9; + OsmPath testPath = new OsmPath( null, startPath, link, null, guideTrack != null, routingContext ); + testPath.setAirDistanceCostAdjustment( (int)( nextNode.calcDistance( endPos ) * airDistanceCostFactor ) ); + if ( wp.radius < minradius ) + { + bestPath = testPath; + minradius = wp.radius; + bestLink = link; + } + } + if ( bestLink != null ) + { + bestLink.addLinkHolder( bestPath ); + } + bestPath.treedepth = 1; + + return bestPath; + } + finally + { + routingContext.unsetWaypoint(); + } + } + + private OsmPath getEndPath( OsmNode n1, OsmLink link, OsmNodeNamed wp, OsmNode endPos ) + { + try + { + if ( wp != null ) routingContext.setWaypoint( wp, true ); + OsmLink startLink = new OsmLink(); + startLink.targetNode = n1; + OsmPath startPath = new OsmPath( startLink ); + startLink.addLinkHolder( startPath ); + + if ( wp != null ) wp.radius = 1e-5; + + OsmPath testPath = new OsmPath( n1, startPath, link, null, guideTrack != null, routingContext ); + testPath.setAirDistanceCostAdjustment( 0 ); + + return testPath; + } + finally + { + if ( wp != null ) routingContext.unsetWaypoint(); + } + } + + private OsmTrack findTrack( String operationName, MatchedWaypoint startWp, MatchedWaypoint endWp, OsmTrack costCuttingTrack, OsmTrack refTrack, boolean reducedTimeoutWhenUnmatched ) + { + boolean verbose = guideTrack != null; + + int maxTotalCost = 1000000000; + + logInfo( "findtrack with maxTotalCost=" + maxTotalCost + " airDistanceCostFactor=" + airDistanceCostFactor ); + + matchPath = null; + int nodesVisited = 0; + + resetCache(); + long endNodeId1 = endWp.node1.getIdFromPos(); + long endNodeId2 = endWp.node2.getIdFromPos(); + long startNodeId1 = startWp.node1.getIdFromPos(); + long startNodeId2 = startWp.node2.getIdFromPos(); + + OsmNode endPos = endWp.crosspoint; + + boolean sameSegmentSearch = ( startNodeId1 == endNodeId1 && startNodeId2 == endNodeId2 ) + || ( startNodeId1 == endNodeId2 && startNodeId2 == endNodeId1 ); + + OsmNode start1 = getStartNode( startNodeId1 ); + OsmNode start2 = getStartNode( startNodeId2 ); + if ( start1 == null || start2 == null ) return null; + + OsmPath startPath1 = getStartPath( start1, start2, startWp, endWp, sameSegmentSearch ); + OsmPath startPath2 = getStartPath( start2, start1, startWp, endWp, sameSegmentSearch ); + + int maxAdjCostFromQueue = 0; + + synchronized( openSet ) + { + openSet.clear(); + if ( startPath1.cost >= 0 ) openSet.add( startPath1 ); + if ( startPath2.cost >= 0 ) openSet.add( startPath2 ); + } + while(!terminated) + { + if ( maxRunningTime > 0 ) + { + long timeout = ( matchPath == null && reducedTimeoutWhenUnmatched ) ? maxRunningTime/3 : maxRunningTime; + if ( System.currentTimeMillis() - startTime > timeout ) + { + throw new IllegalArgumentException( operationName + " timeout after " + (timeout/1000) + " seconds" ); + } + } + OsmPath path = null; + synchronized( openSet ) + { + if ( openSet.size() == 0 ) break; + path = openSet.first(); + openSet.remove( path ); + } + + if ( path.adjustedCost < maxAdjCostFromQueue && airDistanceCostFactor == 0.) + { + throw new RuntimeException( "assertion failed: path.adjustedCost < maxAdjCostFromQueue: " + path.adjustedCost + "<" + maxAdjCostFromQueue ); + } + maxAdjCostFromQueue = path.adjustedCost; + + nodesVisited++; + linksProcessed++; + + OsmLink currentLink = path.getLink(); + OsmNode currentNode = currentLink.targetNode; + OsmNode sourceNode = path.getSourceNode(); + + long currentNodeId = currentNode.getIdFromPos(); + if ( sourceNode != null ) + { + long sourceNodeId = sourceNode.getIdFromPos(); + if ( ( sourceNodeId == endNodeId1 && currentNodeId == endNodeId2 ) + || ( sourceNodeId == endNodeId2 && currentNodeId == endNodeId1 ) ) + { + // track found, compile + logInfo( "found track at cost " + path.cost + " nodesVisited = " + nodesVisited ); + return compileTrack( path, verbose ); + } + } + + // recheck cutoff before doing expensive stuff + int airDistance2 = currentNode.calcDistance( endPos ); + if ( path.cost + airDistance2 > maxTotalCost + 10 ) + { + continue; + } + + if ( !currentNode.wasProcessed ) + { + expandHollowLinkTargets( currentNode, true ); + nodesMap.removeCompletedNodes(); + } + + if ( sourceNode != null ) + { + sourceNode.unlinkLink ( currentLink ); + } + + OsmLink counterLink = null; + for( OsmLink link = currentNode.firstlink; link != null; link = link.next ) + { + OsmNode nextNode = link.targetNode; + + if ( nextNode.isHollow() ) + { + continue; // border node? + } + if ( nextNode.firstlink == null ) + { + continue; // don't care about dead ends + } + if ( nextNode == sourceNode ) + { + counterLink = link; + continue; // border node? + } + + if ( guideTrack != null ) + { + int gidx = path.treedepth + 1; + if ( gidx >= guideTrack.nodes.size() ) + { + continue; + } + OsmPathElement guideNode = guideTrack.nodes.get( gidx ); + if ( nextNode.getILat() != guideNode.getILat() || nextNode.getILon() != guideNode.getILon() ) + { + continue; + } + } + + OsmPath bestPath = null; + + boolean isFinalLink = false; + long targetNodeId = link.targetNode.getIdFromPos(); + if ( currentNodeId == endNodeId1 || currentNodeId == endNodeId2 ) + { + if ( targetNodeId == endNodeId1 || targetNodeId == endNodeId2 ) + { + isFinalLink = true; + } + } + + for( OsmLinkHolder linkHolder = currentLink.firstlinkholder; linkHolder != null; linkHolder = linkHolder.getNextForLink() ) + { + OsmPath otherPath = (OsmPath)linkHolder; + try + { + if ( isFinalLink ) + { + endWp.crosspoint.radius = 1e-5; + routingContext.setWaypoint( endWp.crosspoint, true ); + } + OsmPath testPath = new OsmPath( currentNode, otherPath, link, refTrack, guideTrack != null, routingContext ); + if ( testPath.cost >= 0 && ( bestPath == null || testPath.cost < bestPath.cost ) ) + { + bestPath = testPath; + } + } + finally + { + routingContext.unsetWaypoint(); + } + if ( otherPath != path ) + { + synchronized( openSet ) + { + openSet.remove( otherPath ); + } + } + } + if ( bestPath != null ) + { + int airDistance = isFinalLink ? 0 : nextNode.calcDistance( endPos ); + bestPath.setAirDistanceCostAdjustment( (int)( airDistance * airDistanceCostFactor ) ); + + // check for a match with the cost-cutting-track + if ( costCuttingTrack != null ) + { + OsmPathElement pe = costCuttingTrack.getLink( currentNodeId, targetNodeId ); + if ( pe != null ) + { + int costEstimate = bestPath.cost + + bestPath.elevationCorrection( routingContext ) + + ( costCuttingTrack.cost - pe.cost ); + if ( costEstimate <= maxTotalCost ) + { + matchPath = new OsmPathElement( bestPath ); + } + if ( costEstimate < maxTotalCost ) + { + logInfo( "maxcost " + maxTotalCost + " -> " + costEstimate + " airDistance=" + airDistance ); + maxTotalCost = costEstimate; + } + } + } + + if ( isFinalLink || bestPath.cost + airDistance <= maxTotalCost + 10 ) + { + // add only if this may beat an existing path for that link + OsmLinkHolder dominator = link.firstlinkholder; + while( dominator != null ) + { + if ( bestPath.definitlyWorseThan( (OsmPath)dominator, routingContext ) ) + { + break; + } + dominator = dominator.getNextForLink(); + } + + if ( dominator == null ) + { + bestPath.treedepth = path.treedepth + 1; + link.addLinkHolder( bestPath ); + synchronized( openSet ) + { + openSet.add( bestPath ); + } + } + } + } + } + // if the counterlink does not yet have a path, remove it + if ( counterLink != null && counterLink.firstlinkholder == null ) + { + currentNode.unlinkLink(counterLink); + } + + } + return null; + } + + private void preloadPosition( OsmNode n, int minRingWidth, int minCount ) + { + int c = 0; + int ring = 0; + while( ring <= minRingWidth || ( c < minCount && ring <= 5 ) ) + { + c += preloadRing( n, ring++ ); + } + } + + private int preloadRing( OsmNode n, int ring ) + { + int d = 12500; + int c = 0; + for( int idxLat=-ring; idxLat<=ring; idxLat++ ) + for( int idxLon=-ring; idxLon<=ring; idxLon++ ) + { + int absLat = idxLat < 0 ? -idxLat : idxLat; + int absLon = idxLon < 0 ? -idxLon : idxLon; + int max = absLat > absLon ? absLat : absLon; + if ( max < ring ) continue; + c += nodesCache.loadSegmentFor( n.ilon + d*idxLon , n.ilat +d*idxLat ); + } + return c; + } + + private OsmTrack compileTrack( OsmPath path, boolean verbose ) + { + OsmPathElement element = new OsmPathElement( path ); + + // for final track, cut endnode + if ( guideTrack != null ) element = element.origin; + + OsmTrack track = new OsmTrack(); + track.cost = path.cost; + + int distance = 0; + double ascend = 0; + double ehb = 0.; + + short ele_start = Short.MIN_VALUE; + short ele_end = Short.MIN_VALUE; + + while ( element != null ) + { + track.addNode( element ); + OsmPathElement nextElement = element.origin; + + short ele = element.getSElev(); + if ( ele != Short.MIN_VALUE ) ele_start = ele; + if ( ele_end == Short.MIN_VALUE ) ele_end = ele; + + if ( nextElement != null ) + { + distance += element.calcDistance( nextElement ); + short ele_next = nextElement.getSElev(); + if ( ele_next != Short.MIN_VALUE ) + { + ehb = ehb + (ele - ele_next)/4.; + } + if ( ehb > 10. ) + { + ascend += ehb-10.; + ehb = 10.; + } + else if ( ehb < 0. ) + { + ehb = 0.; + } + } + element = nextElement ; + } + ascend += ehb; + track.distance = distance; + track.ascend = (int)ascend; + track.plainAscend = ( ele_end - ele_start ) / 4; + logInfo( "track-length = " + track.distance ); + logInfo( "filtered ascend = " + track.ascend ); + track.buildMap(); + return track; + } + + private OsmTrack mergeTrack( OsmPathElement match, OsmTrack oldTrack ) + { + + OsmPathElement element = match; + OsmTrack track = new OsmTrack(); + + while ( element != null ) + { + track.addNode( element ); + element = element.origin ; + } + long lastId = 0; + long id1 = match.getIdFromPos(); + long id0 = match.origin == null ? 0 : match.origin.getIdFromPos(); + boolean appending = false; + for( OsmPathElement n : oldTrack.nodes ) + { + if ( appending ) + { + track.nodes.add( n ); + } + + long id = n.getIdFromPos(); + if ( id == id1 && lastId == id0 ) + { + appending = true; + } + lastId = id; + } + + + track.buildMap(); + return track; + } + + public int[] getOpenSet() + { + synchronized( openSet ) + { + return openSet.getExtract(); + } + } + + public boolean isFinished() + { + return finished; + } + + public int getLinksProcessed() + { + return linksProcessed; + } + + public int getDistance() + { + return foundTrack.distance; + } + + public int getAscend() + { + return foundTrack.ascend; + } + + public int getPlainAscend() + { + return foundTrack.plainAscend; + } + + public OsmTrack getFoundTrack() + { + return foundTrack; + } + + public int getAlternativeIndex() + { + return alternativeIndex; + } + + public OsmTrack getFoundRawTrack() + { + return foundRawTrack; + } + + public String getErrorMessage() + { + return errorMessage; + } + + public void terminate() + { + terminated = true; + } +} diff --git a/brouter-core/src/main/java/btools/router/RoutingMessageHandler.java b/brouter-core/src/main/java/btools/router/RoutingMessageHandler.java new file mode 100644 index 0000000..0d12100 --- /dev/null +++ b/brouter-core/src/main/java/btools/router/RoutingMessageHandler.java @@ -0,0 +1,28 @@ +/** + * Container for routig configs + * + * @author ab + */ +package btools.router; + +import btools.expressions.BExpressionReceiver; + +final class RoutingMessageHandler implements BExpressionReceiver +{ + private int ilon; + private int ilat; + + public void setCurrentPos( int lon, int lat) + { + ilon = lon; + ilat = lat; + } + + + @Override + public void expressionWarning( String context, String message ) + { + System.out.println( "message (lon=" + (ilon-180000000) + " lat=" + (ilat-90000000) + + " context " + context + "): " + message ); + } +} diff --git a/brouter-expressions/pom.xml b/brouter-expressions/pom.xml new file mode 100644 index 0000000..a58eda5 --- /dev/null +++ b/brouter-expressions/pom.xml @@ -0,0 +1,13 @@ + + + 4.0.0 + + org.btools + brouter + 0.98 + ../pom.xml + + brouter-expressions + jar + diff --git a/brouter-expressions/src/main/java/btools/expressions/BExpression.java b/brouter-expressions/src/main/java/btools/expressions/BExpression.java new file mode 100644 index 0000000..80bce4f --- /dev/null +++ b/brouter-expressions/src/main/java/btools/expressions/BExpression.java @@ -0,0 +1,173 @@ +package btools.expressions; + + +final class BExpression +{ + private static final int OR_EXP = 10; + private static final int AND_EXP = 11; + private static final int NOT_EXP = 12; + + private static final int ADD_EXP = 20; + private static final int MULTIPLY_EXP = 21; + private static final int MAX_EXP = 22; + + private static final int SWITCH_EXP = 30; + private static final int ASSIGN_EXP = 31; + private static final int LOOKUP_EXP = 32; + private static final int NUMBER_EXP = 33; + private static final int VARIABLE_EXP = 34; + + private static final int DUMPPOS_EXP = 40; + + private int typ; + private BExpression op1; + private BExpression op2; + private BExpression op3; + private float numberValue; + private int variableIdx; + private int lookupNameIdx; + private int lookupValueIdx; + + // Parse the expression and all subexpression + public static BExpression parse( BExpressionContext ctx, int level ) throws Exception + { + String operator = ctx.parseToken(); + if ( operator == null ) + { + if ( level == 0 ) return null; + else throw new IllegalArgumentException( "unexpected end of file" ); + } + + if ( level == 0 ) + { + if ( !"assign".equals( operator ) ) + { + throw new IllegalArgumentException( "operator " + operator + " is invalid on toplevel (only 'assign' allowed)" ); + } + } + + BExpression exp = new BExpression(); + int nops = 3; + + if ( "switch".equals( operator ) ) + { + exp.typ = SWITCH_EXP; + } + else + { + nops = 2; // check binary expressions + + if ( "or".equals( operator ) ) + { + exp.typ = OR_EXP; + } + else if ( "and".equals( operator ) ) + { + exp.typ = AND_EXP; + } + else if ( "multiply".equals( operator ) ) + { + exp.typ = MULTIPLY_EXP; + } + else if ( "add".equals( operator ) ) + { + exp.typ = ADD_EXP; + } + else if ( "max".equals( operator ) ) + { + exp.typ = MAX_EXP; + } + else + { + nops = 1; // check unary expressions + if ( "assign".equals( operator ) ) + { + if ( level > 0 ) throw new IllegalArgumentException( "assign operator within expression" ); + exp.typ = ASSIGN_EXP; + String variable = ctx.parseToken(); + if ( variable == null ) throw new IllegalArgumentException( "unexpected end of file" ); + exp.variableIdx = ctx.getVariableIdx( variable, true ); + if ( exp.variableIdx < ctx.getMinWriteIdx() ) throw new IllegalArgumentException( "cannot assign to readonly variable " + variable ); + } + else if ( "not".equals( operator ) ) + { + exp.typ = NOT_EXP; + } + else if ( "dumppos".equals( operator ) ) + { + exp.typ = DUMPPOS_EXP; + } + else + { + nops = 0; // check elemantary expressions + int idx = operator.indexOf( '=' ); + if ( idx >= 0 ) + { + exp.typ = LOOKUP_EXP; + String name = operator.substring( 0, idx ); + String value = operator.substring( idx+1 ); + + exp.lookupNameIdx = ctx.getLookupNameIdx( name ); + if ( exp.lookupNameIdx < 0 ) + { + throw new IllegalArgumentException( "unknown lookup name: " + name ); + } + exp.lookupValueIdx = ctx.getLookupValueIdx( exp.lookupNameIdx, value ); + if ( exp.lookupValueIdx < 0 ) + { + throw new IllegalArgumentException( "unknown lookup value: " + value ); + } + } + else if ( (idx = ctx.getVariableIdx( operator, false )) >= 0 ) + { + exp.typ = VARIABLE_EXP; + exp.variableIdx = idx; + } + else + { + try + { + exp.numberValue = Float.parseFloat( operator ); + exp.typ = NUMBER_EXP; + } + catch( NumberFormatException nfe ) + { + throw new IllegalArgumentException( "unknown expression: " + operator ); + } + } + } + } + } + // parse operands + if ( nops > 0 ) exp.op1 = BExpression.parse( ctx, level+1 ); + if ( nops > 1 ) exp.op2 = BExpression.parse( ctx, level+1 ); + if ( nops > 2 ) exp.op3 = BExpression.parse( ctx, level+1 ); + return exp; + } + + // Evaluate the expression + public float evaluate( BExpressionContext ctx ) + { + switch( typ ) + { + case OR_EXP: return op1.evaluate(ctx) != 0.f ? 1.f : ( op2.evaluate(ctx) != 0.f ? 1.f : 0.f ); + case AND_EXP: return op1.evaluate(ctx) != 0.f ? ( op2.evaluate(ctx) != 0.f ? 1.f : 0.f ) : 0.f; + case ADD_EXP: return op1.evaluate(ctx) + op2.evaluate(ctx); + case MULTIPLY_EXP: return op1.evaluate(ctx) * op2.evaluate(ctx); + case MAX_EXP: return max( op1.evaluate(ctx), op2.evaluate(ctx) ); + case SWITCH_EXP: return op1.evaluate(ctx) != 0.f ? op2.evaluate(ctx) : op3.evaluate(ctx); + case ASSIGN_EXP: return ctx.assign( variableIdx, op1.evaluate(ctx) ); + case LOOKUP_EXP: return ctx.getLookupMatch( lookupNameIdx, lookupValueIdx ); + case NUMBER_EXP: return numberValue; + case VARIABLE_EXP: return ctx.getVariableValue( variableIdx ); + case NOT_EXP: return op1.evaluate(ctx) == 0.f ? 1.f : 0.f; + case DUMPPOS_EXP: ctx.expressionWarning( "INFO" ); return op1.evaluate(ctx); + default: throw new IllegalArgumentException( "unknown op-code: " + typ ); + } + } + + private float max( float v1, float v2 ) + { + return v1 > v2 ? v1 : v2; + } +} diff --git a/brouter-expressions/src/main/java/btools/expressions/BExpressionContext.java b/brouter-expressions/src/main/java/btools/expressions/BExpressionContext.java new file mode 100644 index 0000000..7b2f0c3 --- /dev/null +++ b/brouter-expressions/src/main/java/btools/expressions/BExpressionContext.java @@ -0,0 +1,620 @@ +// context for simple expression +// context means: +// - the local variables +// - the local variable names +// - the lookup-input variables + +package btools.expressions; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.TreeMap; + +public final class BExpressionContext +{ + private static final String CONTEXT_TAG = "---context:"; + private static final String VERSION_TAG = "---lookupversion:"; + + private String context; + private boolean _inOurContext = false; + private BufferedReader _br = null; + private boolean _readerDone = false; + + private BExpressionReceiver _receiver; + + private Map lookupNumbers = new HashMap(); + private ArrayList lookupValues = new ArrayList(); + private ArrayList lookupNames = new ArrayList(); + private ArrayList lookupHistograms = new ArrayList(); + + private boolean lookupDataFrozen = false; + + private int[] lookupData = new int[0]; + + private Map variableNumbers = new HashMap(); + + private float[] variableData; + + + // hash-cache for function results + private long[] _arrayBitmap; + private int currentHashBucket = -1; + private long currentBitmap = 0; + + public List expressionList; + + private int minWriteIdx; + + // build-in variable indexes for fast access + private int costfactorIdx; + private int turncostIdx; + private int initialcostIdx; + + private float[] _arrayCostfactor; + private float[] _arrayTurncost; + private float[] _arrayInitialcost; + + public float getCostfactor() { return _arrayCostfactor[currentHashBucket]; } + public float getTurncost() { return _arrayTurncost[currentHashBucket]; } + public float getInitialcost() { return _arrayInitialcost[currentHashBucket]; } + + private int linenr; + + public short lookupVersion = -1; + + public BExpressionContext( String context ) + { + this( context, 4096 ); + } + + /** + * Create an Expression-Context for the given node + * + * @param context global, way or node - context of that instance + * @param hashSize size of hashmap for result caching + */ + public BExpressionContext( String context, int hashSize ) + { + this.context = context; + _arrayBitmap = new long[hashSize]; + + _arrayCostfactor = new float[hashSize]; + _arrayTurncost = new float[hashSize]; + _arrayInitialcost = new float[hashSize]; + } + + + /** + * encode lookup data to a 64-bit word + */ + public long encode( int[] ld ) + { + long w = 0; + for( int inum = 0; inum < lookupValues.size(); inum++ ) // loop over lookup names + { + int n = lookupValues.get(inum).length - 1; + int d = ld[inum]; + if ( n == 2 ) { n = 1; d = d == 2 ? 1 : 0; } // 1-bit encoding for booleans + + while( n != 0 ) { n >>= 1; w <<= 1; } + w |= (long)d; + } + return w; + } + + /** + * decode a 64-bit word into a lookup data array + */ + public void decode( int[] ld, long w ) + { + for( int inum = lookupValues.size()-1; inum >= 0; inum-- ) // loop over lookup names + { + int nv = lookupValues.get(inum).length; + int n = nv == 3 ? 1 : nv-1; // 1-bit encoding for booleans + int m = 0; + long ww = w; + while( n != 0 ) { n >>= 1; ww >>= 1; m = m<<1 | 1; } + int d = (int)(w & m); + if ( nv == 3 && d == 1 ) d = 2; // 1-bit encoding for booleans + ld[inum] = d; + w = ww; + } + } + + /** + * much like decode, but just for counting bits + */ + private void countBits() + { + int bits = 0; + for( int inum = lookupValues.size()-1; inum >= 0; inum-- ) // loop over lookup names + { + int nv = lookupValues.get(inum).length; + int n = nv == 3 ? 1 : nv-1; // 1-bit encoding for booleans + while( n != 0 ) { n >>= 1; bits++; } + } +// System.out.println( "context=" + context + ",bits=" + bits + " keys=" + lookupValues.size() ); + if ( bits > 64 ) throw new IllegalArgumentException( "lookup table for context " + context + " exceeds 64 bits!" ); + } + + public String getCsvDescription( long bitmap ) + { + StringBuilder sb = new StringBuilder( 200 ); + decode( lookupData, bitmap ); + for( int inum = 0; inum < lookupValues.size(); inum++ ) // loop over lookup names + { + BExpressionLookupValue[] va = lookupValues.get(inum); + sb.append( '\t' ).append( va[lookupData[inum]].toString() ); + } + return sb.toString(); + } + + public String getCsvHeader() + { + StringBuilder sb = new StringBuilder( 200 ); + for( String name: lookupNames ) + { + sb.append( '\t' ).append( name ); + } + return sb.toString(); + } + + public void readMetaData( File lookupsFile ) + { + try + { + BufferedReader br = new BufferedReader( new FileReader( lookupsFile ) ); + + int parsedLines = 0; + boolean ourContext = false; + for(;;) + { + String line = br.readLine(); + if ( line == null ) break; + line = line.trim(); + if ( line.length() == 0 || line.startsWith( "#" ) ) continue; + if ( line.startsWith( CONTEXT_TAG ) ) + { + ourContext = line.substring( CONTEXT_TAG.length() ).equals( context ); + continue; + } + if ( line.startsWith( VERSION_TAG ) ) + { + lookupVersion = Short.parseShort( line.substring( VERSION_TAG.length() ) ); + continue; + } + if ( !ourContext ) continue; + parsedLines++; + StringTokenizer tk = new StringTokenizer( line, " " ); + String name = tk.nextToken(); + String value = tk.nextToken(); + int idx = name.indexOf( ';' ); + if ( idx >= 0 ) name = name.substring( 0, idx ); + BExpressionLookupValue newValue = addLookupValue( name, value, null ); + + // add aliases + while( newValue != null && tk.hasMoreTokens() ) newValue.addAlias( tk.nextToken() ); + } + br.close(); + if ( parsedLines == 0 && !"global".equals(context) ) + { + throw new IllegalArgumentException( lookupsFile.getAbsolutePath() + + " does not contain data for context " + context + " (old version?)" ); + } + + // post-process metadata: + lookupDataFrozen = true; + countBits(); + } + catch( Exception e ) + { + throw new RuntimeException( e ); + } + } + + private void evaluate( int[] lookupData2 ) + { + lookupData = lookupData2; + for( BExpression exp: expressionList) + { + exp.evaluate( this ); + } + } + + private static int[] crctable = { + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, + 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, + 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, + 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, + 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, + 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, + 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, + 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, + 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, + 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, + 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, + 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, + 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, + 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, + 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, + 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, + 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, + 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, + 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, + }; + + + public void evaluate( long bitmap, BExpressionReceiver receiver ) + { + _receiver = receiver; + + if ( currentBitmap != bitmap || currentHashBucket < 0 ) + { + // calc hash bucket from crc + int crc = 0xFFFFFFFF; + long bm = bitmap; + for( int j=0; j<8; j++ ) + { + crc = (crc >>> 8) ^ crctable[(crc ^ (int)bm) & 0xff]; + bm >>= 8; + } + int hashSize = _arrayBitmap.length; + currentHashBucket = (crc & 0xfffffff) % hashSize; + currentBitmap = bitmap; + } + + if ( _arrayBitmap[currentHashBucket] == bitmap ) + { + return; + } + + _arrayBitmap[currentHashBucket] = bitmap; + + decode( lookupData, bitmap ); + evaluate( lookupData ); + + _arrayCostfactor[currentHashBucket] = variableData[costfactorIdx]; + _arrayTurncost[currentHashBucket] = variableData[turncostIdx]; + _arrayInitialcost[currentHashBucket] = variableData[initialcostIdx]; + + _receiver = null; + } + + public void dumpStatistics() + { + TreeMap counts = new TreeMap(); + // first count + for( String name: lookupNumbers.keySet() ) + { + int cnt = 0; + int inum = lookupNumbers.get(name).intValue(); + int[] histo = lookupHistograms.get(inum); +// if ( histo.length == 500 ) continue; + for( int i=2; i 0 ) + { + String key = counts.lastEntry().getKey(); + String name = counts.get(key); + counts.remove( key ); + int inum = lookupNumbers.get(name).intValue(); + BExpressionLookupValue[] values = lookupValues.get(inum); + int[] histo = lookupHistograms.get(inum); + if ( values.length == 1000 ) continue; + String[] svalues = new String[values.length]; + for( int i=0; i=0; i-- ) + { + System.out.println( name + ";" + svalues[i] ); + } + } + } + + /** + * @return a new lookupData array, or null if no metadata defined + */ + public int[] createNewLookupData() + { + if ( lookupDataFrozen ) + { + return new int[lookupValues.size()]; + } + return null; + } + + /** + * add a new lookup-value for the given name to the given lookupData array. + * If no array is given (null value passed), the value is added to + * the context-binded array. In that case, unknown names and values are + * created dynamically. + * + * @return a newly created value element, if any, to optionally add aliases + */ + public BExpressionLookupValue addLookupValue( String name, String value, int[] lookupData2 ) + { + BExpressionLookupValue newValue = null; + Integer num = lookupNumbers.get( name ); + if ( num == null ) + { + if ( lookupData2 != null ) + { + // do not create unknown name for external data array + return newValue; + } + + // unknown name, create + num = new Integer( lookupValues.size() ); + lookupNumbers.put( name, num ); + lookupNames.add( name ); + lookupValues.add( new BExpressionLookupValue[]{ new BExpressionLookupValue( "" ) + , new BExpressionLookupValue( "unknown" ) } ); + lookupHistograms.add( new int[2] ); + int[] ndata = new int[lookupData.length+1]; + System.arraycopy( lookupData, 0, ndata, 0, lookupData.length ); + lookupData = ndata; + } + + // look for that value + int inum = num.intValue(); + BExpressionLookupValue[] values = lookupValues.get( inum ); + int[] histo = lookupHistograms.get( inum ); + int i=0; + for( ; i _parseFile( File file ) throws Exception + { + _br = new BufferedReader( new FileReader( file ) ); + _readerDone = false; + List result = new ArrayList(); + for(;;) + { + BExpression exp = BExpression.parse( this, 0 ); + if ( exp == null ) break; + result.add( exp ); + } + _br.close(); + _br = null; + return result; + } + + + public float getVariableValue( String name, float defaultValue ) + { + Integer num = variableNumbers.get( name ); + return num == null ? defaultValue : getVariableValue( num.intValue() ); + } + + public float getVariableValue( String name ) + { + Integer num = variableNumbers.get( name ); + return num == null ? 0.f : getVariableValue( num.intValue() ); + } + + public float getVariableValue( int variableIdx ) + { + return variableData[variableIdx]; + } + + public int getVariableIdx( String name, boolean create ) + { + Integer num = variableNumbers.get( name ); + if ( num == null ) + { + if ( create ) + { + num = new Integer( variableNumbers.size() ); + variableNumbers.put( name, num ); + } + else + { + return -1; + } + } + return num.intValue(); + } + + public int getMinWriteIdx() + { + return minWriteIdx; + } + + public float getLookupMatch( int nameIdx, int valueIdx ) + { + return lookupData[nameIdx] == valueIdx ? 1.0f : 0.0f; + } + + public int getLookupNameIdx( String name ) + { + Integer num = lookupNumbers.get( name ); + return num == null ? -1 : num.intValue(); + } + + public int getLookupValueIdx( int nameIdx, String value ) + { + BExpressionLookupValue[] values = lookupValues.get( nameIdx ); + for( int i=0; i< values.length; i++ ) + { + if ( values[i].equals( value ) ) return i; + } + return -1; + } + + + public String parseToken() throws Exception + { + for(;;) + { + String token = _parseToken(); + if ( token == null ) return null; + if ( token.startsWith( CONTEXT_TAG ) ) + { + _inOurContext = token.substring( CONTEXT_TAG.length() ).equals( context ); + } + else if ( _inOurContext ) + { + return token; + } + } + } + + + private String _parseToken() throws Exception + { + StringBuilder sb = new StringBuilder(32); + boolean inComment = false; + for(;;) + { + int ic = _readerDone ? -1 : _br.read(); + if ( ic < 0 ) + { + if ( sb.length() == 0 ) return null; + _readerDone = true; + return sb.toString(); + } + char c = (char)ic; + if ( c == '\n' ) linenr++; + + if ( inComment ) + { + if ( c == '\r' || c == '\n' ) inComment = false; + continue; + } + if ( Character.isWhitespace( c ) ) + { + if ( sb.length() > 0 ) return sb.toString(); + else continue; + } + if ( c == '#' && sb.length() == 0 ) inComment = true; + else sb.append( c ); + } + } + + public float assign( int variableIdx, float value ) + { + variableData[variableIdx] = value; + return value; + } + + public void expressionWarning( String message ) + { + _arrayBitmap[currentHashBucket] = 0L; // no caching if warnings + if ( _receiver != null ) _receiver.expressionWarning( context, message ); + } +} diff --git a/brouter-expressions/src/main/java/btools/expressions/BExpressionLookupValue.java b/brouter-expressions/src/main/java/btools/expressions/BExpressionLookupValue.java new file mode 100644 index 0000000..8043d3c --- /dev/null +++ b/brouter-expressions/src/main/java/btools/expressions/BExpressionLookupValue.java @@ -0,0 +1,66 @@ +/** + * A lookup value with optional aliases + * + * toString just gives the primary value, + * equals just compares against primary value + * matches() also compares aliases + * + * @author ab + */ +package btools.expressions; + +import java.util.ArrayList; + +final class BExpressionLookupValue +{ + String value; + ArrayList aliases; + + @Override + public String toString() + { + return value; + } + + public BExpressionLookupValue( String value ) + { + this.value = value; + } + + public void addAlias( String alias ) + { + if ( aliases == null ) aliases = new ArrayList(); + aliases.add( alias ); + } + + @Override + public boolean equals( Object o ) + { + if ( o instanceof String ) + { + String v = (String)o; + return value.equals( v ); + } + if ( o instanceof BExpressionLookupValue ) + { + BExpressionLookupValue v = (BExpressionLookupValue)o; + + return value.equals( v.value ); + } + return false; + } + + public boolean matches( String s ) + { + if ( value.equals( s ) ) return true; + if ( aliases != null ) + { + for( String alias : aliases ) + { + if ( alias.equals( s ) ) return true; + } + } + return false; + } + +} diff --git a/brouter-expressions/src/main/java/btools/expressions/BExpressionReceiver.java b/brouter-expressions/src/main/java/btools/expressions/BExpressionReceiver.java new file mode 100644 index 0000000..57b44f6 --- /dev/null +++ b/brouter-expressions/src/main/java/btools/expressions/BExpressionReceiver.java @@ -0,0 +1,7 @@ +package btools.expressions; + + +public interface BExpressionReceiver +{ + public void expressionWarning( String context, String message ); +} diff --git a/brouter-map-creator/pom.xml b/brouter-map-creator/pom.xml new file mode 100644 index 0000000..3b93909 --- /dev/null +++ b/brouter-map-creator/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + org.btools + brouter + 0.98 + ../pom.xml + + brouter-map-creator + jar + + + + org.btools + brouter-util + ${project.version} + + + org.btools + brouter-expressions + ${project.version} + + + org.btools + brouter-mapaccess + ${project.version} + + + junit + junit + + + diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/MapCreatorBase.java b/brouter-map-creator/src/main/java/btools/mapcreator/MapCreatorBase.java new file mode 100644 index 0000000..70e4052 --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/MapCreatorBase.java @@ -0,0 +1,166 @@ +/** + * common base class for the map-filters + * + * @author ab + */ +package btools.mapcreator; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.util.HashMap; + +public abstract class MapCreatorBase implements WayListener, NodeListener, RelationListener +{ + private DataOutputStream[] tileOutStreams; + protected File outTileDir; + + protected HashMap tags; + + public void putTag( String key, String value ) + { + if ( tags == null ) tags = new HashMap(); + tags.put( key, value ); + } + + public String getTag( String key ) + { + return tags == null ? null : tags.get( key ); + } + + public HashMap getTagsOrNull() + { + return tags; + } + + public void setTags( HashMap tags ) + { + this.tags = tags; + } + + protected static long readId( DataInputStream is) throws IOException + { + int offset = is.readByte(); + if ( offset == 32 ) return -1; + long i = is.readInt(); + i = i << 5; + return i | offset; + } + + protected static void writeId( DataOutputStream o, long id ) throws IOException + { + if ( id == -1 ) + { + o.writeByte( 32 ); + return; + } + int offset = (int)( id & 0x1f ); + int i = (int)( id >> 5 ); + o.writeByte( offset ); + o.writeInt( i ); + } + + + protected static File[] sortBySizeAsc( File[] files ) + { + int n = files.length; + long[] sizes = new long[n]; + File[] sorted = new File[n]; + for( int i=0; i " ); + return; + } + new NodeCutter().process( new File( args[0] ), new File( args[1] ) ); + } + + public void process( File nodeTilesIn, File nodeTilesOut ) throws Exception + { + this.outTileDir = nodeTilesOut; + + new NodeIterator( this, true ).processDir( nodeTilesIn, ".tlf" ); + } + + @Override + public void nodeFileStart( File nodefile ) throws Exception + { + lonoffset = -1; + latoffset = -1; + } + + @Override + public void nextNode( NodeData n ) throws Exception + { + n.writeTo( getOutStreamForTile( getTileIndex( n.ilon, n.ilat ) ) ); + } + + @Override + public void nodeFileEnd( File nodeFile ) throws Exception + { + closeTileOutStreams(); + } + + private int getTileIndex( int ilon, int ilat ) + { + int lonoff = (ilon / 45000000 ) * 45; + int latoff = (ilat / 30000000 ) * 30; + if ( lonoffset == -1 ) lonoffset = lonoff; + if ( latoffset == -1 ) latoffset = latoff; + if ( lonoff != lonoffset || latoff != latoffset ) + throw new IllegalArgumentException( "inconsistent node: " + ilon + " " + ilat ); + + int lon = (ilon / 5000000) % 9; + int lat = (ilat / 5000000) % 6; + if ( lon < 0 || lon > 8 || lat < 0 || lat > 5 ) throw new IllegalArgumentException( "illegal pos: " + ilon + "," + ilat ); + return lon*6 + lat; + } + + + protected String getNameForTile( int tileIndex ) + { + int lon = (tileIndex / 6 ) * 5 + lonoffset - 180; + int lat = (tileIndex % 6 ) * 5 + latoffset - 90; + String slon = lon < 0 ? "W" + (-lon) : "E" + lon; + String slat = lat < 0 ? "S" + (-lat) : "N" + lat; + return slon + "_" + slat + ".n5d"; + } + +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/NodeData.java b/brouter-map-creator/src/main/java/btools/mapcreator/NodeData.java new file mode 100644 index 0000000..5b0b388 --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/NodeData.java @@ -0,0 +1,55 @@ +package btools.mapcreator; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.HashMap; + +import btools.util.*; + +/** + * Container for node data on the preprocessor level + * + * @author ab + */ +public class NodeData extends MapCreatorBase +{ + public long nid; + public int ilon; + public int ilat; + public long description; + public short selev = Short.MIN_VALUE; + + public NodeData( long id, double lon, double lat ) + { + nid = id; + ilat = (int)( ( lat + 90. )*1000000. + 0.5); + ilon = (int)( ( lon + 180. )*1000000. + 0.5); + } + + public NodeData( DataInputStream dis ) throws Exception + { + nid = readId( dis ); + ilon = dis.readInt(); + ilat = dis.readInt(); + int mode = dis.readByte(); + if ( ( mode & 1 ) != 0 ) description = dis.readLong(); + if ( ( mode & 2 ) != 0 ) selev = dis.readShort(); + } + + public void writeTo( DataOutputStream dos ) throws Exception + { + writeId( dos, nid ); + dos.writeInt( ilon ); + dos.writeInt( ilat ); + int mode = ( description == 0L ? 0 : 1 ) | ( selev == Short.MIN_VALUE ? 0 : 2 ); + dos.writeByte( (byte)mode ); + if ( ( mode & 1 ) != 0 ) dos.writeLong( description ); + if ( ( mode & 2 ) != 0 ) dos.writeShort( selev ); + } +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/NodeFilter.java b/brouter-map-creator/src/main/java/btools/mapcreator/NodeFilter.java new file mode 100644 index 0000000..f04761d --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/NodeFilter.java @@ -0,0 +1,86 @@ +package btools.mapcreator; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.HashMap; + +import btools.util.*; + +/** + * NodeFilter does 1 step in map-processing: + * + * - filters out unused nodes according to the way file + * + * @author ab + */ +public class NodeFilter extends MapCreatorBase +{ + private DataOutputStream nodesOutStream; + private File nodeTilesOut; + protected DenseLongMap nodebitmap; + + public static void main(String[] args) throws Exception + { + System.out.println("*** NodeFilter: Filter way related nodes"); + if (args.length != 3) + { + System.out.println("usage: java NodeFilter " ); + return; + } + + new NodeFilter().process( new File( args[0] ), new File( args[1] ), new File( args[2] ) ); + } + + public void process( File nodeTilesIn, File wayFileIn, File nodeTilesOut ) throws Exception + { + this.nodeTilesOut = nodeTilesOut; + + // read the wayfile into a bitmap of used nodes + nodebitmap = Boolean.getBoolean( "useDenseMaps" ) ? new DenseLongMap( 1 ) : new TinyDenseLongMap(); + new WayIterator( this, false ).processFile( wayFileIn ); + + // finally filter all node files + new NodeIterator( this, true ).processDir( nodeTilesIn, ".tls" ); + } + + @Override + public void nextWay( WayData data ) throws Exception + { + int nnodes = data.nodes.size(); + for (int i=0; i bit set, -1 -> unset + { + n.writeTo( nodesOutStream ); + } + } + + @Override + public void nodeFileEnd( File nodeFile ) throws Exception + { + nodesOutStream.close(); + } +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/NodeIterator.java b/brouter-map-creator/src/main/java/btools/mapcreator/NodeIterator.java new file mode 100644 index 0000000..e251755 --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/NodeIterator.java @@ -0,0 +1,76 @@ +package btools.mapcreator; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.HashMap; + +import btools.util.*; + +/** + * Iterate over a singe nodefile or a directory + * of nodetiles and feed the nodes to the callback listener + * + * @author ab + */ +public class NodeIterator extends MapCreatorBase +{ + private NodeListener listener; + private boolean delete; + + public NodeIterator( NodeListener nodeListener, boolean deleteAfterReading ) + { + listener = nodeListener; + delete = deleteAfterReading; + } + + public void processDir( File indir, String inSuffix ) throws Exception + { + if ( !indir.isDirectory() ) + { + throw new IllegalArgumentException( "not a directory: " + indir ); + } + + File[] af = sortBySizeAsc( indir.listFiles() ); + for( int i=0; i | java OsmCutter "); + System.out.println("or : java OsmCutter "); + return; + } + + new OsmCutter().process( + new File( args[0] ) + , new File( args[1] ) + , new File( args[2] ) + , new File( args[3] ) + , args.length > 4 ? new File( args[4] ) : null ); + } + + private BExpressionContext _expctxWay; + private BExpressionContext _expctxNode; + + public void process (File lookupFile, File outTileDir, File wayFile, File relFile, File mapFile) throws Exception + { + if ( !lookupFile.exists() ) + { + throw new IllegalArgumentException( "lookup-file: " + lookupFile + " does not exist" ); + } + + _expctxWay = new BExpressionContext("way"); + _expctxWay.readMetaData( lookupFile ); + + _expctxNode = new BExpressionContext("node"); + _expctxNode.readMetaData( lookupFile ); + + this.outTileDir = outTileDir; + if ( !outTileDir.isDirectory() ) throw new RuntimeException( "out tile directory " + outTileDir + " does not exist" ); + + wayDos = new DataOutputStream( new BufferedOutputStream( new FileOutputStream( wayFile ) ) ); + cyclewayDos = new DataOutputStream( new BufferedOutputStream( new FileOutputStream( relFile ) ) ); + + // read the osm map into memory + long t0 = System.currentTimeMillis(); + new OsmParser().readMap( mapFile, this, this, this ); + long t1 = System.currentTimeMillis(); + + System.out.println( "parsing time (ms) =" + (t1-t0) ); + + // close all files + closeTileOutStreams(); + wayDos.close(); + cyclewayDos.close(); + +/* System.out.println( "-------- way-statistics -------- " ); + _expctxWay.dumpStatistics(); + System.out.println( "-------- node-statistics -------- " ); + _expctxNode.dumpStatistics(); +*/ + System.out.println( statsLine() ); + } + + private void checkStats() + { + if ( (++recordCnt % 100000) == 0 ) System.out.println( statsLine() ); + } + + private String statsLine() + { + return "records read: " + recordCnt + " nodes=" + nodesParsed + " ways=" + waysParsed + " rels=" + relsParsed + " changesets=" + changesetsParsed; + } + + + @Override + public void nextNode( NodeData n ) throws Exception + { + nodesParsed++; + checkStats(); + + if ( n.getTagsOrNull() != null ) + { + int[] lookupData = _expctxNode.createNewLookupData(); + for( String key : n.getTagsOrNull().keySet() ) + { + String value = n.getTag( key ); + _expctxNode.addLookupValue( key, value, lookupData ); + } + n.description = _expctxNode.encode(lookupData); + } + // write node to file + int tileIndex = getTileIndex( n.ilon, n.ilat ); + if ( tileIndex >= 0 ) + { + DataOutputStream dos = getOutStreamForTile( tileIndex ); + n.writeTo( dos ); + } + } + + + @Override + public void nextWay( WayData w ) throws Exception + { + waysParsed++; + checkStats(); + + // filter out non-highway ways + if ( w.getTag( "highway" ) == null ) + { + // ... but eventually fake a ferry tag + if ( "ferry".equals( w.getTag( "route" ) ) ) + { + w.putTag( "highway", "ferry" ); + } + else + { + return; + } + } + + // encode tags + if ( w.getTagsOrNull() != null ) + { + int[] lookupData = _expctxWay.createNewLookupData(); + for( String key : w.getTagsOrNull().keySet() ) + { + String value = w.getTag( key ); + _expctxWay.addLookupValue( key, value, lookupData ); + } + w.description = _expctxWay.encode(lookupData); + } + + w.writeTo( wayDos ); + } + + @Override + public void nextRelation( RelationData r ) throws Exception + { + relsParsed++; + checkStats(); + + // filter out non-cycle relations + if ( ! "bicycle".equals( r.getTag( "route" ) ) ) + { + return; + } + + for ( int i=0; i 7 || lat < 0 || lat > 5 ) + { + System.out.println( "warning: ignoring illegal pos: " + ilon + "," + ilat ); + return -1; + } + return lon*6 + lat; + } + + protected String getNameForTile( int tileIndex ) + { + int lon = (tileIndex / 6 ) * 45 - 180; + int lat = (tileIndex % 6 ) * 30 - 90; + String slon = lon < 0 ? "W" + (-lon) : "E" + lon; + String slat = lat < 0 ? "S" + (-lat) : "N" + lat; + return slon + "_" + slat + ".tls"; + } +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/OsmLinkP.java b/brouter-map-creator/src/main/java/btools/mapcreator/OsmLinkP.java new file mode 100644 index 0000000..141f43c --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/OsmLinkP.java @@ -0,0 +1,29 @@ +/** + * Container for link between two Osm nodes (pre-pocessor version) + * + * @author ab + */ +package btools.mapcreator; + + +public final class OsmLinkP +{ + /** + * The description bitmap is mainly the way description + * used to calculate the costfactor + */ + public long descriptionBitmap; + + /** + * The target is either the next link or the target node + */ + public OsmNodeP targetNode; + + public OsmLinkP next; + + + public boolean counterLinkWritten( ) + { + return descriptionBitmap == 0L; + } +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/OsmNodeP.java b/brouter-map-creator/src/main/java/btools/mapcreator/OsmNodeP.java new file mode 100644 index 0000000..3cd27aa --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/OsmNodeP.java @@ -0,0 +1,243 @@ +/** + * Container for an osm node (pre-pocessor version) + * + * @author ab + */ +package btools.mapcreator; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class OsmNodeP implements Comparable +{ + public static final int EXTERNAL_BITMASK = 0x80; + public static final int FIRSTFORWAY_BITMASK = 0x40; + public static final int TRANSFERNODE_BITMASK = 0x20; + public static final int WRITEDESC_BITMASK = 0x10; + public static final int SKIPDETAILS_BITMASK = 0x08; + public static final int NODEDESC_BITMASK = 0x04; + + /** + * The latitude + */ + public int ilat; + + /** + * The longitude + */ + public int ilon; + + + /** + * The links to other nodes + */ + public OsmLinkP firstlink = null; + + + /** + * The elevation + */ + public short selev; + + public boolean isBorder = false; + + public byte wayAndBits = -1; // use for bridge/tunnel logic + + + // interface OsmPos + public int getILat() + { + return ilat; + } + + public int getILon() + { + return ilon; + } + + public short getSElev() + { + // if all bridge or all tunnel, elevation=no-data + return (wayAndBits & 24) == 0 ? selev : Short.MIN_VALUE; + } + + public double getElev() + { + return selev / 4.; + } + + + + public void addLink( OsmLinkP link ) + { + if ( firstlink != null ) link.next = firstlink; + firstlink = link; + } + + public long getNodeDecsription() + { + return 0L; + } + + public void writeNodeData( DataOutputStream os ) throws IOException + { + int lonIdx = ilon/62500; + int latIdx = ilat/62500; + + // buffer the body to first calc size + ByteArrayOutputStream bos = new ByteArrayOutputStream( ); + DataOutputStream os2 = new DataOutputStream( bos ); + + os2.writeShort( getSElev() ); + + int nlinks = 0; + + // hack: write node-desc as link tag (copy cycleway-bits) + long nodeDescription = getNodeDecsription(); + + for( OsmLinkP link0 = firstlink; link0 != null; link0 = link0.next ) + { + nlinks++; + OsmLinkP link = link0; + OsmNodeP origin = this; + int skipDetailBit = link0.counterLinkWritten() ? SKIPDETAILS_BITMASK : 0; + + // first pass just to see if that link is consistent + while( link != null ) + { + OsmNodeP target = link.targetNode; + if ( !target.isTransferNode() ) + { + break; + } + // next link is the one (of two), does does'nt point back + for( link = target.firstlink; link != null; link = link.next ) + { + if ( link.targetNode != origin ) break; + } + origin = target; + } + if ( link == null ) continue; // dead end + + if ( skipDetailBit == 0) + { + link = link0; + } + origin = this; + long lastDescription = 0; + while( link != null ) + { + OsmNodeP target = link.targetNode; + int tranferbit = target.isTransferNode() ? TRANSFERNODE_BITMASK : 0; + int writedescbit = link.descriptionBitmap != lastDescription ? WRITEDESC_BITMASK : 0; + int nodedescbit = nodeDescription != 0L ? NODEDESC_BITMASK : 0; + + if ( skipDetailBit != 0 ) + { + writedescbit = 0; + } + int targetLonIdx = target.ilon/62500; + int targetLatIdx = target.ilat/62500; + + if ( targetLonIdx == lonIdx && targetLatIdx == latIdx ) + { + // reduced position for internal target + os2.writeByte( tranferbit | writedescbit | nodedescbit | skipDetailBit ); + os2.writeShort( (short)(target.ilon - lonIdx*62500 - 31250) ); + os2.writeShort( (short)(target.ilat - latIdx*62500 - 31250) ); + } + else + { + // full position for external target + os2.writeByte( tranferbit | writedescbit | nodedescbit | skipDetailBit | EXTERNAL_BITMASK ); + os2.writeInt( target.ilon ); + os2.writeInt( target.ilat ); + } + if ( writedescbit != 0 ) + { + os2.writeLong( link.descriptionBitmap ); + } + if ( nodedescbit != 0 ) + { + os2.writeLong( nodeDescription ); + nodeDescription = 0L; + } + lastDescription = link.descriptionBitmap; + + if ( tranferbit == 0) + { + target.markLinkWritten( origin ); + break; + } + os2.writeShort( target.getSElev() ); + // next link is the one (of two), does does'nt point back + for( link = target.firstlink; link != null; link = link.next ) + { + if ( link.targetNode != origin ) break; + } + if ( link == null ) throw new RuntimeException( "follow-up link not found for transfer-node!" ); + origin = target; + } + } + + // calculate the body size + int bodySize = bos.size(); + + os.writeShort( (short)(ilon - lonIdx*62500 - 31250) ); + os.writeShort( (short)(ilat - latIdx*62500 - 31250) ); + os.writeInt( bodySize ); + bos.writeTo( os ); + } + + public String toString2() + { + return (ilon-180000000) + "_" + (ilat-90000000) + "_" + (selev/4); + } + + public long getIdFromPos() + { + return ((long)ilon)<<32 | ilat; + } + + public boolean isTransferNode() + { + return (!isBorder) && _linkCnt() == 2; + } + + private int _linkCnt() + { + int cnt = 0; + + for( OsmLinkP link = firstlink; link != null; link = link.next ) + { + cnt++; + } + return cnt; + } + + // mark the link to the given node as written, + // don't want to write the counter-direction + // in full details + public void markLinkWritten( OsmNodeP t ) + { + for( OsmLinkP link = firstlink; link != null; link = link.next ) + { + if ( link.targetNode == t) link.descriptionBitmap = 0L; + } + } + + /** + * Compares two OsmNodes for position ordering. + * + * @return -1,0,1 depending an comparson result + */ + public int compareTo( OsmNodeP n ) + { + long id1 = getIdFromPos(); + long id2 = n.getIdFromPos(); + if ( id1 < id2 ) return -1; + if ( id1 > id2 ) return 1; + return 0; + } +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/OsmNodePT.java b/brouter-map-creator/src/main/java/btools/mapcreator/OsmNodePT.java new file mode 100644 index 0000000..41316b2 --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/OsmNodePT.java @@ -0,0 +1,39 @@ +/** + * Container for an osm node with tags (pre-pocessor version) + * + * @author ab + */ +package btools.mapcreator; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class OsmNodePT extends OsmNodeP +{ + public long descriptionBits; + + public byte wayOrBits = 0; // used to propagate bike networks to nodes + + public OsmNodePT() + { + } + + public OsmNodePT( long descriptionBits ) + { + this.descriptionBits = descriptionBits; + } + + @Override + public long getNodeDecsription() + { + return descriptionBits | (long)( (wayOrBits & 6) >> 1 ); + } + + @Override + public boolean isTransferNode() + { + return false; // always have descriptionBits so never transfernode + } + +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/OsmParser.java b/brouter-map-creator/src/main/java/btools/mapcreator/OsmParser.java new file mode 100644 index 0000000..7731cad --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/OsmParser.java @@ -0,0 +1,240 @@ +package btools.mapcreator; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.zip.GZIPInputStream; + +/** + * Parser for OSM data + * + * @author ab + */ +public class OsmParser extends MapCreatorBase +{ + private BufferedReader _br; + + private NodeListener nListener; + private WayListener wListener; + private RelationListener rListener; + + public void readMap( File mapFile, + NodeListener nListener, + WayListener wListener, + RelationListener rListener ) throws Exception + { + + this.nListener = nListener; + this.wListener = wListener; + this.rListener = rListener; + + if ( mapFile == null ) + { + _br = new BufferedReader(new InputStreamReader(System.in)); + } + else + { + if ( mapFile.getName().endsWith( ".gz" ) ) + { + _br = new BufferedReader(new InputStreamReader( new GZIPInputStream( new FileInputStream( mapFile ) ) ) ); + } + else + { + _br = new BufferedReader(new InputStreamReader( new FileInputStream( mapFile ) ) ); + } + } + + for(;;) + { + String line = _br.readLine(); + if ( line == null ) break; + + if ( checkNode( line ) ) continue; + if ( checkWay( line ) ) continue; + if ( checkRelation( line ) ) continue; + if ( checkChangeset( line ) ) continue; + } + + if ( mapFile != null ) + { + _br.close(); + } + } + + + private boolean checkNode( String line ) throws Exception + { + int idx0 = line.indexOf( "" ) ) + { + // read additional tags + for(;;) + { + String l2 = _br.readLine(); + if ( l2 == null ) return false; + + int i2; + if ( (i2 = l2.indexOf( "= 0 ) + { // property-tag + i2 += 8; + int ri2 = l2.indexOf( '"', i2 ); + String key = l2.substring( i2, ri2 ); + i2 = l2.indexOf( " v=\"", ri2 ); + if ( i2 >= 0 ) + { + i2 += 4; + int ri3 = l2.indexOf( '"', i2 ); + String value = l2.substring( i2, ri3 ); + + n.putTag( key, value ); + } + } + else if ( l2.indexOf( "" ) >= 0 ) + { // end-tag + break; + } + } + } + nListener.nextNode( n ); + return true; + } + + + private boolean checkWay( String line ) throws Exception + { + int idx0 = line.indexOf( "= 0 ) + { // node reference + i2 += 9; + int ri2 = l2.indexOf( '"', i2 ); + long nid = Long.parseLong( l2.substring( i2, ri2 ) ); + w.nodes.add( nid ); + } + else if ( (i2 = l2.indexOf( "= 0 ) + { // property-tag + i2 += 8; + int ri2 = l2.indexOf( '"', i2 ); + String key = l2.substring( i2, ri2 ); + i2 = l2.indexOf( " v=\"", ri2 ); + if ( i2 >= 0 ) + { + i2 += 4; + int ri3 = l2.indexOf( '"', i2 ); + String value = l2.substring( i2, ri3 ); + w.putTag( key, value ); + } + } + else if ( l2.indexOf( "" ) >= 0 ) + { // end-tag + break; + } + } + wListener.nextWay( w ); + return true; + } + + private boolean checkChangeset( String line ) throws Exception + { + int idx0 = line.indexOf( "" ) ) + { + int loopcheck = 0; + for(;;) + { + String l2 = _br.readLine(); + if ( l2.indexOf("") >= 0 || ++loopcheck > 10000 ) break; + } + } + return true; + } + + private boolean checkRelation( String line ) throws Exception + { + int idx0 = line.indexOf( "= 0 ) + { // node reference + i2 += 24; + int ri2 = l2.indexOf( '"', i2 ); + long wid = Long.parseLong( l2.substring( i2, ri2 ) ); + r.ways.add( wid ); + } + else if ( (i2 = l2.indexOf( "= 0 ) + { // property-tag + i2 += 8; + int ri2 = l2.indexOf( '"', i2 ); + String key = l2.substring( i2, ri2 ); + i2 = l2.indexOf( " v=\"", ri2 ); + if ( i2 >= 0 ) + { + i2 += 4; + int ri3 = l2.indexOf( '"', i2 ); + String value = l2.substring( i2, ri3 ); + r.putTag( key, value ); + } + } + else if ( l2.indexOf( "" ) >= 0 ) + { // end-tag + break; + } + } + rListener.nextRelation( r ); + return true; + } + +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/PosUnifier.java b/brouter-map-creator/src/main/java/btools/mapcreator/PosUnifier.java new file mode 100644 index 0000000..16963cc --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/PosUnifier.java @@ -0,0 +1,203 @@ +package btools.mapcreator; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.HashMap; + +import btools.util.CompactLongSet; +import btools.util.CompactLongMap; +import btools.util.FrozenLongSet; +import btools.util.FrozenLongMap; + +/** + * PosUnifier does 3 steps in map-processing: + * + * - unify positions + * - add srtm elevation data + * - make a bordernodes file containing net data + * from the bordernids-file just containing ids + * + * @author ab + */ +public class PosUnifier extends MapCreatorBase +{ + private DataOutputStream nodesOutStream; + private DataOutputStream borderNodesOut; + private File nodeTilesOut; + private CompactLongSet positionSet; + + private HashMap srtmmap ; + private int lastStrmLonIdx; + private int lastStrmLatIdx; + private SrtmData lastSrtmData; + private String srtmdir; + + private int totalLatSteps = 0; + private int totalLonSteps = 0; + + private CompactLongSet borderNids; + + + public static void main(String[] args) throws Exception + { + System.out.println("*** PosUnifier: Unify position values and enhance elevation"); + if (args.length != 5) + { + System.out.println("usage: java PosUnifier " ); + return; + } + new PosUnifier().process( new File( args[0] ), new File( args[1] ), new File( args[2] ), new File( args[3] ), args[4] ); + } + + public void process( File nodeTilesIn, File nodeTilesOut, File bordernidsinfile, File bordernodesoutfile, String srtmdir ) throws Exception + { + this.nodeTilesOut = nodeTilesOut; + this.srtmdir = srtmdir; + + // read border nids set + DataInputStream dis = createInStream( bordernidsinfile ); + borderNids = new CompactLongSet(); + try + { + for(;;) + { + long nid = readId( dis ); + if ( !borderNids.contains( nid ) ) borderNids.fastAdd( nid ); + } + } + catch( EOFException eof ) + { + dis.close(); + } + borderNids = new FrozenLongSet( borderNids ); + + // process all files + borderNodesOut = createOutStream( bordernodesoutfile ); + new NodeIterator( this, true ).processDir( nodeTilesIn, ".n5d" ); + borderNodesOut.close(); + } + + @Override + public void nodeFileStart( File nodefile ) throws Exception + { + resetSrtm(); + + nodesOutStream = createOutStream( fileFromTemplate( nodefile, nodeTilesOut, "u5d" ) ); + + positionSet = new CompactLongSet(); + } + + @Override + public void nextNode( NodeData n ) throws Exception + { + SrtmData srtm = srtmForNode( n.ilon, n.ilat ); + n.selev = srtm == null ? Short.MIN_VALUE : srtm.getElevation( n.ilon, n.ilat); + + findUniquePos( n ); + + n.writeTo( nodesOutStream ); + if ( borderNids.contains( n.nid ) ) + { + n.writeTo( borderNodesOut ); + } + } + + @Override + public void nodeFileEnd( File nodeFile ) throws Exception + { + nodesOutStream.close(); + } + + private void findUniquePos( NodeData n ) + { + // fix the position for uniqueness + int lonmod = n.ilon % 1000000; + int londelta = lonmod < 500000 ? 1 : -1; + int latmod = n.ilat % 1000000; + int latdelta = latmod < 500000 ? 1 : -1; + for(int latsteps = 0; latsteps < 100; latsteps++) + { + for(int lonsteps = 0; lonsteps <= latsteps; lonsteps++) + { + int lon = n.ilon + lonsteps*londelta; + int lat = n.ilat + latsteps*latdelta; + long pid = ((long)lon)<<32 | lat; // id from position + if ( !positionSet.contains( pid ) ) + { + totalLonSteps += lonsteps; + totalLatSteps += latsteps; + positionSet.fastAdd( pid ); + n.ilon = lon; + n.ilat = lat; + return; + } + } + } + System.out.println( "*** WARNING: cannot unify position for: " + n.ilon + " " + n.ilat ); + } + + + /** + * get the srtm data set for a position + * srtm coords are srtm__ + * where srtmLon = 180 + lon, srtmLat = 60 - lat + */ + private SrtmData srtmForNode( int ilon, int ilat ) throws Exception + { + int srtmLonIdx = (ilon+5000000)/5000000; + int srtmLatIdx = (154999999-ilat)/5000000; + + if ( srtmLatIdx < 1 || srtmLatIdx > 24 || srtmLonIdx < 1 || srtmLonIdx > 72 ) + { + return null; + } + if ( srtmLonIdx == lastStrmLonIdx && srtmLatIdx == lastStrmLatIdx ) + { + return lastSrtmData; + } + lastStrmLonIdx = srtmLonIdx; + lastStrmLatIdx = srtmLatIdx; + + StringBuilder sb = new StringBuilder( 16 ); + sb.append( "srtm_" ); + sb.append( (char)('0' + srtmLonIdx/10 ) ).append( (char)('0' + srtmLonIdx%10 ) ).append( '_' ); + sb.append( (char)('0' + srtmLatIdx/10 ) ).append( (char)('0' + srtmLatIdx%10 ) ).append( ".zip" ); + String filename = sb.toString(); + + + lastSrtmData = srtmmap.get( filename ); + if ( lastSrtmData == null && !srtmmap.containsKey( filename ) ) + { + File f = new File( new File( srtmdir ), filename ); + System.out.println( "reading: " + f + " ilon=" + ilon + " ilat=" + ilat ); + if ( f.exists() ) + { + try + { + lastSrtmData = new SrtmData( f ); + } + catch( Exception e ) + { + System.out.println( "**** ERROR reading " + f + " ****" ); + } + } + srtmmap.put( filename, lastSrtmData ); + } + return lastSrtmData; + } + + private void resetSrtm() + { + srtmmap = new HashMap(); + lastStrmLonIdx = -1; + lastStrmLatIdx = -1; + lastSrtmData = null; + } + +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/RelationData.java b/brouter-map-creator/src/main/java/btools/mapcreator/RelationData.java new file mode 100644 index 0000000..864204f --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/RelationData.java @@ -0,0 +1,37 @@ +package btools.mapcreator; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.HashMap; + +import btools.util.*; + +/** + * Container for relation data on the preprocessor level + * + * @author ab + */ +public class RelationData extends MapCreatorBase +{ + public long rid; + public long description; + public LongList ways; + + public RelationData( long id ) + { + rid = id; + ways = new LongList( 16 ); + } + + public RelationData( long id, LongList ways ) + { + rid = id; + this.ways = ways; + } +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/RelationListener.java b/brouter-map-creator/src/main/java/btools/mapcreator/RelationListener.java new file mode 100644 index 0000000..2789714 --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/RelationListener.java @@ -0,0 +1,13 @@ +package btools.mapcreator; + +import java.io.File; + +/** + * Callbacklistener for Relations + * + * @author ab + */ +public interface RelationListener +{ + void nextRelation( RelationData data ) throws Exception; +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/SrtmData.java b/brouter-map-creator/src/main/java/btools/mapcreator/SrtmData.java new file mode 100644 index 0000000..fca1b3a --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/SrtmData.java @@ -0,0 +1,174 @@ +/** + * This is a wrapper for a 5*5 degree srtm file in ascii/zip-format + * + * - filter out unused nodes according to the way file + * - enhance with SRTM elevation data + * - split further in smaller (5*5 degree) tiles + * + * @author ab + */ +package btools.mapcreator; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + + +public class SrtmData +{ + public int ncols; + public int nrows; + public double xllcorner; + public double yllcorner; + public double cellsize; + public int nodata_value; + public short[] eval_array; + + private double minlon; + private double maxlon; + private double minlat; + private double maxlat; + + public void init() + { + minlon = xllcorner; + maxlon = minlon + cellsize*ncols; + minlat = yllcorner; + maxlat = minlat + cellsize*nrows; + } + + private boolean missingData = false; + + public short getElevation( int ilon, int ilat ) + { + double lon = ilon / 1000000. - 180.; + double lat = ilat / 1000000. - 90.; + + double dcol = (lon - minlon)/cellsize -0.5; + double drow = (lat - minlat)/cellsize -0.5; + int row = (int)drow; + int col = (int)dcol; + if ( col < 0 ) col = 0; + if ( col >= ncols-1 ) col = ncols - 2; + if ( row < 0 ) row = 0; + if ( row >= nrows-1 ) row = nrows - 2; + double wrow = drow-row; + double wcol = dcol-col; + missingData = false; + double eval = (1.-wrow)*(1.-wcol)*get(row ,col ) + + ( wrow)*(1.-wcol)*get(row+1,col ) + + (1.-wrow)*( wcol)*get(row ,col+1) + + ( wrow)*( wcol)*get(row+1,col+1); + return missingData ? Short.MIN_VALUE : (short)(eval); + } + + private short get( int r, int c ) + { + short e = eval_array[r*ncols + c ]; + if ( e == Short.MIN_VALUE ) missingData = true; + return e; + } + + public SrtmData( File file ) throws Exception + { + ZipInputStream zis = new ZipInputStream( new FileInputStream( file ) ); + try + { + for(;;) + { + ZipEntry ze = zis.getNextEntry(); + if ( ze.getName().endsWith( ".asc" ) ) + { + readFromStream( zis ); + +/* // test + int[] ca = new int[]{ 50477121, 8051915, // 181 + 50477742, 8047408, // 154 + 50477189, 8047308, // 159 + }; + for( int i=0; i " ); + + return; + } + new WayCutter().process( new File( args[0] ), new File( args[1] ), new File( args[2] ), new File( args[3] ) ); + } + + public void process( File nodeTilesIn, File wayFileIn, File wayTilesOut, File relationFileIn ) throws Exception + { + this.outTileDir = wayTilesOut; + + // *** read the relation file into a set (currently cycleway processing only) + cyclewayset = new CompactLongSet(); + DataInputStream dis = createInStream( relationFileIn ); + try + { + for(;;) + { + long wid = readId( dis ); + if ( !cyclewayset.contains( wid ) ) cyclewayset.add( wid ); + } + } + catch( EOFException eof ) + { + dis.close(); + } + System.out.println( "marked cycleways: " + cyclewayset.size() ); + + + // *** read all nodes into tileIndexMap + tileIndexMap = Boolean.getBoolean( "useDenseMaps" ) ? new DenseLongMap( 6 ) : new TinyDenseLongMap(); + new NodeIterator( this, false ).processDir( nodeTilesIn, ".tlf" ); + + // *** finally process the way-file, cutting into pieces + new WayIterator( this, true ).processFile( wayFileIn ); + closeTileOutStreams(); + } + + @Override + public void nextNode( NodeData n ) throws Exception + { + tileIndexMap.put( n.nid, getTileIndex( n.ilon, n.ilat ) ); + } + + @Override + public void nextWay( WayData data ) throws Exception + { + // propagate the cycleway-bit + if ( cyclewayset.contains( data.wid ) ) + { + data.description |= 2; + } + + long waytileset = 0; + int nnodes = data.nodes.size(); + + // determine the tile-index for each node + for (int i=0; i 7 || lat < 0 || lat > 5 ) throw new IllegalArgumentException( "illegal pos: " + ilon + "," + ilat ); + return lon*6 + lat; + } + + protected String getNameForTile( int tileIndex ) + { + int lon = (tileIndex / 6 ) * 45 - 180; + int lat = (tileIndex % 6 ) * 30 - 90; + String slon = lon < 0 ? "W" + (-lon) : "E" + lon; + String slat = lat < 0 ? "S" + (-lat) : "N" + lat; + return slon + "_" + slat + ".wtl"; + } + +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/WayCutter5.java b/brouter-map-creator/src/main/java/btools/mapcreator/WayCutter5.java new file mode 100644 index 0000000..ea4aea5 --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/WayCutter5.java @@ -0,0 +1,146 @@ +package btools.mapcreator; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.HashMap; + +import btools.util.*; + +/** + * WayCutter5 does 2 step in map-processing: + * + * - cut the 45*30 way files into 5*5 pieces + * - create a file containing all border node ids + * + * @author ab + */ +public class WayCutter5 extends MapCreatorBase +{ + private DataOutputStream borderNidsOutStream; + private DenseLongMap tileIndexMap; + private File nodeTilesIn; + private int lonoffset; + private int latoffset; + + public static void main(String[] args) throws Exception + { + System.out.println("*** WayCutter5: Soft-Cut way-data into tiles"); + if (args.length != 4) + { + System.out.println("usage: java WayCutter5 " ); + return; + } + new WayCutter5().process( new File( args[0] ), new File( args[1] ), new File( args[2] ), new File( args[3] ) ); + } + + public void process( File nodeTilesIn, File wayTilesIn, File wayTilesOut, File borderNidsOut ) throws Exception + { + this.nodeTilesIn = nodeTilesIn; + this.outTileDir = wayTilesOut; + + borderNidsOutStream = createOutStream( borderNidsOut ); + + new WayIterator( this, true ).processDir( wayTilesIn, ".wtl" ); + + borderNidsOutStream.close(); + } + + @Override + public void wayFileStart( File wayfile ) throws Exception + { + // read corresponding node-file into tileIndexMap + String name = wayfile.getName(); + String nodefilename = name.substring( 0, name.length()-3 ) + "tlf"; + File nodefile = new File( nodeTilesIn, nodefilename ); + + tileIndexMap = Boolean.getBoolean( "useDenseMaps" ) ? new DenseLongMap( 6 ) : new TinyDenseLongMap(); + lonoffset = -1; + latoffset = -1; + new NodeIterator( this, false ).processFile( nodefile ); + } + + @Override + public void nextNode( NodeData n ) throws Exception + { + tileIndexMap.put( n.nid, getTileIndex( n.ilon, n.ilat ) ); + } + + @Override + public void nextWay( WayData data ) throws Exception + { + long waytileset = 0; + int nnodes = data.nodes.size(); + int[] tiForNode = new int[nnodes]; + + // determine the tile-index for each node + for (int i=0; i 0 && tiForNode[i-1] != ti ) || (i+1 < nnodes && tiForNode[i+1] != ti ) ) + { + writeId( borderNidsOutStream, data.nodes.get(i) ); + } + } + } + } + + @Override + public void wayFileEnd( File wayFile ) throws Exception + { + closeTileOutStreams(); + } + + private int getTileIndex( int ilon, int ilat ) + { + int lonoff = (ilon / 45000000 ) * 45; + int latoff = (ilat / 30000000 ) * 30; + if ( lonoffset == -1 ) lonoffset = lonoff; + if ( latoffset == -1 ) latoffset = latoff; + if ( lonoff != lonoffset || latoff != latoffset ) + throw new IllegalArgumentException( "inconsistent node: " + ilon + " " + ilat ); + + int lon = (ilon / 5000000) % 9; + int lat = (ilat / 5000000) % 6; + if ( lon < 0 || lon > 8 || lat < 0 || lat > 5 ) throw new IllegalArgumentException( "illegal pos: " + ilon + "," + ilat ); + return lon*6 + lat; + } + + + protected String getNameForTile( int tileIndex ) + { + int lon = (tileIndex / 6 ) * 5 + lonoffset - 180; + int lat = (tileIndex % 6 ) * 5 + latoffset - 90; + String slon = lon < 0 ? "W" + (-lon) : "E" + lon; + String slat = lat < 0 ? "S" + (-lat) : "N" + lat; + return slon + "_" + slat + ".wt5"; + } +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/WayData.java b/brouter-map-creator/src/main/java/btools/mapcreator/WayData.java new file mode 100644 index 0000000..05e691c --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/WayData.java @@ -0,0 +1,62 @@ +package btools.mapcreator; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.HashMap; + +import btools.util.*; + +/** + * Container for waydata on the preprocessor level + * + * @author ab + */ +public class WayData extends MapCreatorBase +{ + public long wid; + public long description; + public LongList nodes; + + public WayData( long id ) + { + wid = id; + nodes = new LongList( 16 ); + } + + public WayData( long id, LongList nodes ) + { + wid = id; + this.nodes = nodes; + } + + public WayData( DataInputStream di ) throws Exception + { + nodes = new LongList( 16 ); + wid = readId( di) ; + description = di.readLong(); + for (;;) + { + long nid = readId( di ); + if ( nid == -1 ) break; + nodes.add( nid ); + } + } + + public void writeTo( DataOutputStream dos ) throws Exception + { + writeId( dos, wid ); + dos.writeLong( description ); + int size = nodes.size(); + for( int i=0; i < size; i++ ) + { + writeId( dos, nodes.get( i ) ); + } + writeId( dos, -1 ); // stopbyte + } +} diff --git a/brouter-map-creator/src/main/java/btools/mapcreator/WayIterator.java b/brouter-map-creator/src/main/java/btools/mapcreator/WayIterator.java new file mode 100644 index 0000000..89fed53 --- /dev/null +++ b/brouter-map-creator/src/main/java/btools/mapcreator/WayIterator.java @@ -0,0 +1,76 @@ +package btools.mapcreator; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.HashMap; + +import btools.util.*; + +/** + * Iterate over a singe wayfile or a directory + * of waytiles and feed the ways to the callback listener + * + * @author ab + */ +public class WayIterator extends MapCreatorBase +{ + private WayListener listener; + private boolean delete; + + public WayIterator( WayListener wayListener, boolean deleteAfterReading ) + { + listener = wayListener; + delete = deleteAfterReading; + } + + public void processDir( File indir, String inSuffix ) throws Exception + { + if ( !indir.isDirectory() ) + { + throw new IllegalArgumentException( "not a directory: " + indir ); + } + + File[] af = sortBySizeAsc( indir.listFiles() ); + for( int i=0; i nodesMap; + private List nodesList; + private CompactLongSet borderSet; + private short lookupVersion; + + private BExpressionContext expctxWay; + + private int minLon; + private int minLat; + + private void reset() + { + minLon = -1; + minLat = -1; + nodesMap = new CompactLongMap(); + borderSet = new CompactLongSet(); + } + + public static void main(String[] args) throws Exception + { + System.out.println("*** WayLinker: Format a regionof an OSM map for routing"); + if (args.length != 7) + { + System.out.println("usage: java WayLinker "); + return; + } + new WayLinker().process( new File( args[0] ), new File( args[1] ), new File( args[2] ), new File( args[3] ), new File( args[4] ), new File( args[5] ), args[6] ); + } + + public void process( File nodeTilesIn, File wayTilesIn, File borderFileIn, File lookupFile, File profileFile, File dataTilesOut, String dataTilesSuffix ) throws Exception + { + this.nodeTilesIn = nodeTilesIn; + this.dataTilesOut = dataTilesOut; + this.borderFileIn = borderFileIn; + this.dataTilesSuffix = dataTilesSuffix; + + // read lookup file to get the lookup-version + expctxWay = new BExpressionContext("way"); + expctxWay.readMetaData( lookupFile ); + lookupVersion = expctxWay.lookupVersion; + expctxWay.parseFile( profileFile, "global" ); + + // then process all segments + new WayIterator( this, true ).processDir( wayTilesIn, ".wt5" ); + } + + @Override + public void wayFileStart( File wayfile ) throws Exception + { + // process corresponding node-file, if any + File nodeFile = fileFromTemplate( wayfile, nodeTilesIn, "u5d" ); + if ( nodeFile.exists() ) + { + reset(); + + // read the border file + readingBorder = true; + new NodeIterator( this, false ).processFile( borderFileIn ); + borderSet = new FrozenLongSet( borderSet ); + + // read this tile's nodes + readingBorder = false; + new NodeIterator( this, false ).processFile( nodeFile ); + + // freeze the nodes-map + FrozenLongMap nodesMapFrozen = new FrozenLongMap( nodesMap ); + nodesMap = nodesMapFrozen; + nodesList = nodesMapFrozen.getValueList(); + } + } + + @Override + public void nextNode( NodeData data ) throws Exception + { + OsmNodeP n = data.description == 0L ? new OsmNodeP() : new OsmNodePT(data.description); + n.ilon = data.ilon; + n.ilat = data.ilat; + n.selev = data.selev; + n.isBorder = readingBorder; + if ( readingBorder || (!borderSet.contains( data.nid )) ) + { + nodesMap.fastPut( data.nid, n ); + } + + if ( readingBorder ) + { + borderSet.fastAdd( data.nid ); + return; + } + + // remember the segment coords + int min_lon = (n.ilon / 5000000 ) * 5000000; + int min_lat = (n.ilat / 5000000 ) * 5000000; + if ( minLon == -1 ) minLon = min_lon; + if ( minLat == -1 ) minLat = min_lat; + if ( minLat != min_lat || minLon != min_lon ) + throw new IllegalArgumentException( "inconsistent node: " + n.ilon + " " + n.ilat ); + } + + @Override + public void nextWay( WayData way ) throws Exception + { + long description = way.description; + long reverseDescription = description | 1L; // (add reverse bit) + + // filter according to profile + expctxWay.evaluate( description, null ); + boolean ok = expctxWay.getCostfactor() < 10000.; + expctxWay.evaluate( reverseDescription, null ); + ok |= expctxWay.getCostfactor() < 10000.; + + if ( !ok ) return; + + byte lowbyte = (byte)description; + + OsmNodeP n1 = null; + OsmNodeP n2 = null; + for (int i=0; i seglists = new LazyArrayOfLists(nLonSegs*nLatSegs); + for( OsmNodeP n : nodesList ) + { + if ( n == null || n.firstlink == null || n.isTransferNode() ) continue; + if ( n.ilon < minLon || n.ilon >= maxLon + || n.ilat < minLat || n.ilat >= maxLat ) continue; + int lonIdx = (n.ilon-minLon)/1000000; + int latIdx = (n.ilat-minLat)/1000000; + + int tileIndex = lonIdx * nLatSegs + latIdx; + seglists.getList(tileIndex).add( n ); + } + nodesList = null; + seglists.trimAll(); + + // open the output file + File outfile = fileFromTemplate( wayfile, dataTilesOut, dataTilesSuffix ); + DataOutputStream os = createOutStream( outfile ); + + // write 5*5 index dummy + long[] fileIndex = new long[25]; + for( int i55=0; i55<25; i55++) + { + os.writeLong( 0 ); + } + long filepos = 200L; + + // sort further in 1/80-degree squares + for( int lonIdx = 0; lonIdx < nLonSegs; lonIdx++ ) + { + for( int latIdx = 0; latIdx < nLatSegs; latIdx++ ) + { + int tileIndex = lonIdx * nLatSegs + latIdx; + if ( seglists.getSize(tileIndex) > 0 ) + { + List nlist = seglists.getList(tileIndex); + + LazyArrayOfLists subs = new LazyArrayOfLists(6400); + byte[][] subByteArrays = new byte[6400][]; + for( int ni=0; ni subList = subs.getList(si); + if ( subList.size() > 0 ) + { + Collections.sort( subList ); + + ByteArrayOutputStream bos = new ByteArrayOutputStream( ); + DataOutputStream dos = new DataOutputStream( bos ); + dos.writeInt( subList.size() ); + for( int ni=0; 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

tJMdWBr}5wv}NrFvr8*q4WfJOMSv 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 literal 0 HcmV?d00001 diff --git a/brouter-map-creator/src/test/resources/lookups.dat b/brouter-map-creator/src/test/resources/lookups.dat new file mode 100644 index 0000000..6bf1a31 --- /dev/null +++ b/brouter-map-creator/src/test/resources/lookups.dat @@ -0,0 +1,317 @@ +---lookupversion:2 + +---context:way + +highway;0001731794 track +highway;0001457935 residential +highway;0000968516 service +highway;0000756237 footway +highway;0000521566 path +highway;0000261772 unclassified +highway;0000220315 secondary +highway;0000207585 tertiary +highway;0000103445 steps +highway;0000102114 primary +highway;0000094484 cycleway +highway;0000090388 living_street +highway;0000035041 motorway +highway;0000029965 pedestrian +highway;0000026875 motorway_link +highway;0000015054 trunk +highway;0000014604 primary_link +highway;0000012211 road +highway;0000011822 trunk_link +highway;0000005882 construction +highway;0000005425 bridleway +highway;0000005180 secondary_link +highway;0000003360 platform +highway;0000002616 proposed abandoned +highway;0000001374 tertiary_link +highway;0000000760 ferry +highway;0000000541 raceway +highway;0000000346 rest_area +highway;0000000300 bus_stop +highway;0000000184 services + +tracktype;0000356503 grade2 +tracktype;0000353482 grade3 +tracktype;0000281625 grade1 +tracktype;0000245193 grade4 +tracktype;0000179135 grade5 + +surface;0000363915 asphalt +surface;0000303589 paved +surface;0000196783 gravel +surface;0000137371 ground +surface;0000128215 grass +surface;0000092748 unpaved +surface;0000086579 paving_stones +surface;0000066111 cobblestone +surface;0000042061 dirt +surface;0000026551 concrete +surface;0000025631 compacted +surface;0000019861 sand +surface;0000009400 pebblestone +surface;0000003197 fine_gravel + +maxspeed;0000402224 30 +maxspeed;0000224685 50 +maxspeed;0000045177 100 +maxspeed;0000037529 70 +maxspeed;0000014237 none +maxspeed;0000014022 60 +maxspeed;0000011530 80 +maxspeed;0000009951 10 +maxspeed;0000008056 20 +maxspeed;0000005772 120 +maxspeed;0000003165 40 +maxspeed;0000002987 7 +maxspeed;0000002826 signals +maxspeed;0000001933 130 + +service;0000221481 parking_aisle +service;0000157110 driveway + +lit;0000132495 yes + +lanes;0000098207 2 +lanes;0000042192 1 +lanes;0000018533 3 +lanes;0000004577 4 +lanes;0000000448 5 +lanes;0000000318 1.5 + +access;0000044859 yes permissive +access;0000008452 designated official +access;0000028727 destination customers +access;0000076985 agricultural forestry +access;0000116270 private +access;0000028044 no + +foot;0000339384 yes allowed Yes +foot;0000125339 designated official +foot;0000018945 no +foot;0000001562 private +foot;0000000279 destination +foot;0000008172 permissive + +bicycle;0000302789 yes allowed permissive +bicycle;0000108056 designated official +bicycle;0000000265 destination +bicycle;0000003593 dismount +bicycle;0000001426 private +bicycle;0000070179 no + +motorcar;0000010111 yes permissive +motorcar;0000001537 designated official +motorcar;0000007102 destination +motorcar;0000016706 agricultural forestry agriculture +motorcar;0000002178 private +motorcar;0000077771 no + +motor_vehicle;0000013813 yes permissive +motor_vehicle;0000002098 designated official +motor_vehicle;0000009792 destination +motor_vehicle;0000019301 agricultural forestry +motor_vehicle;0000006563 private +motor_vehicle;0000025491 no + +motorcycle;0000005750 yes permissive +motorcycle;0000001158 designated official +motorcycle;0000005805 destination +motorcycle;0000012401 agricultural forestry +motorcycle;0000001180 private +motorcycle;0000053955 no + +vehicle;0000000505 yes permissive +vehicle;0000000027 designated +vehicle;0000007582 destination +vehicle;0000004357 agricultural forestry +vehicle;0000001155 private +vehicle;0000006487 no + +cycleway;0000033575 track +cycleway;0000012829 no +cycleway;0000011604 lane +cycleway;0000008938 opposite +cycleway;0000001503 none +cycleway;0000001146 right +cycleway;0000001031 opposite_track +cycleway;0000001029 yes +cycleway;0000000856 opposite_lane +cycleway;0000000675 both +cycleway;0000000665 left +cycleway;0000000521 shared +cycleway;0000000383 street +cycleway;0000000176 segregated + +mtb:scale;0000043968 0 +mtb:scale;0000019705 1 +mtb:scale;0000006436 2 +mtb:scale;0000002702 3 +mtb:scale;0000001083 4 +mtb:scale;0000000329 5 + +sac_scale;0000049626 hiking +sac_scale;0000007933 mountain_hiking +sac_scale;0000001160 demanding_mountain_hiking +sac_scale;0000000523 yes +sac_scale;0000000364 alpine_hiking +sac_scale;0000000117 demanding_alpine_hiking + +noexit;0000058492 yes + +motorroad;0000019250 yes + +oneway;0000330245 yes +oneway;0000075148 no +oneway;0000003679 -1 +oneway;0000000001 true +oneway;0000000001 1 + +junction;0000015929 roundabout + +bridge;0000182649 yes viaduct true suspension + +tunnel;0000031626 yes + +lcn;0000018999 yes + +longdistancecycleway;0000000001 yes + +reversedirection;0000000001 yes + +---context:node + +highway;0000100947 turning_circle +highway;0000067645 traffic_signals +highway;0000047209 crossing +highway;0000037164 bus_stop +highway;0000006577 motorway_junction +highway;0000003811 stop +highway;0000002331 mini_roundabout +highway;0000001789 milestone +highway;0000001692 passing_place +highway;0000001289 give_way +highway;0000001092 emergency_access_point +highway;0000000683 speed_camera +highway;0000000672 steps +highway;0000000658 incline_steep +highway;0000000620 elevator +highway;0000000506 street_lamp +highway;0000000490 ford +highway;0000000458 incline +highway;0000000135 rest_area +highway;0000000105 path +highway;0000000098 emergency_bay +highway;0000000096 road +highway;0000000087 platform +highway;0000000074 services +highway;0000000058 track +highway;0000000055 service +highway;0000000054 footway +highway;0000000053 traffic_calming +highway;0000000046 toll_bridge +highway;0000000037 city_entry + +barrier;0000076979 gate +barrier;0000069308 bollard +barrier;0000028131 lift_gate +barrier;0000017332 cycle_barrier +barrier;0000005693 entrance +barrier;0000002885 block +barrier;0000001065 kissing_gate +barrier;0000000828 cattle_grid +barrier;0000000602 stile +barrier;0000000561 turnstile +barrier;0000000512 no +barrier;0000000463 fence +barrier;0000000417 bump_gate +barrier;0000000324 sally_port +barrier;0000000283 yes +barrier;0000000283 hampshire_gate +barrier;0000000236 swing_gate +barrier;0000000203 chain +barrier;0000000181 toll_booth +barrier;0000000180 door +barrier;0000000104 chicane +barrier;0000000096 tree +barrier;0000000087 border_control +barrier;0000000077 log +barrier;0000000076 traffic_crossing_pole +barrier;0000000063 wall +barrier;0000000060 fallen_tree +barrier;0000000052 stone +barrier;0000000048 ditch +barrier;0000000031 spikes + +access;0000001309 yes permissive +access;0000000118 designated official +access;0000000405 destination customers +access;0000000276 agricultural forestry +access;0000008574 private +access;0000002145 no + +foot;0000080681 yes permissive +foot;0000000326 designated official +foot;0000000023 destination +foot;0000000156 private +foot;0000009170 no + +bicycle;0000076717 yes permissive +bicycle;0000000406 designated official +bicycle;0000000018 destination +bicycle;0000000081 dismount +bicycle;0000000051 private +bicycle;0000016121 no + +motorcar;0000005785 yes permissive +motorcar;0000000026 designated official +motorcar;0000000080 destination +motorcar;0000000112 agricultural forestry +motorcar;0000000171 private +motorcar;0000001817 no + +motor_vehicle;0000000066 yes permissive +motor_vehicle;0000000000 designated official +motor_vehicle;0000000030 destination +motor_vehicle;0000000073 agricultural forestry +motor_vehicle;0000000136 private +motor_vehicle;0000000469 no + +motorcycle;0000004515 yes permissive +motorcycle;0000000007 designated official +motorcycle;0000000054 destination +motorcycle;0000000027 agricultural forestry +motorcycle;0000000063 private +motorcycle;0000001637 no + +vehicle;0000000058 yes permissive +vehicle;0000000000 designated +vehicle;0000000081 destination +vehicle;0000000038 agricultural forestry +vehicle;0000000041 private +vehicle;0000000271 no + +crossing;0000032485 traffic_signals +crossing;0000014300 uncontrolled +crossing;0000005086 island +crossing;0000001565 unmarked +crossing;0000001066 no +crossing;0000000333 zebra + +railway;0000034039 level_crossing +railway;0000010175 crossing + +noexit;0000043010 yes + +entrance;0000015094 yes +entrance;0000007079 main +entrance;0000000554 service +entrance;0000000169 emergency +entrance;0000000063 exit +entrance;0000000008 private + +lcn;0000018999 yes + +longdistancecycleway;0000000001 yes diff --git a/brouter-mapaccess/pom.xml b/brouter-mapaccess/pom.xml new file mode 100644 index 0000000..364de1d --- /dev/null +++ b/brouter-mapaccess/pom.xml @@ -0,0 +1,13 @@ + + + 4.0.0 + + org.btools + brouter + 0.98 + ../pom.xml + + brouter-mapaccess + jar + diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/ByteDataReader.java b/brouter-mapaccess/src/main/java/btools/mapaccess/ByteDataReader.java new file mode 100644 index 0000000..b1776ff --- /dev/null +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/ByteDataReader.java @@ -0,0 +1,59 @@ +/** + * fast data-reading from a byte-array + * + * @author ab + */ +package btools.mapaccess; + + +final class ByteDataReader +{ + private byte[] ab; + private int aboffset; + + public ByteDataReader( byte[] byteArray ) + { + ab = byteArray; + } + + public int readInt() + { + int i3 = ab[aboffset++]& 0xff; + int i2 = ab[aboffset++]& 0xff; + int i1 = ab[aboffset++]& 0xff; + int i0 = ab[aboffset++]& 0xff; + return (i3 << 24) + (i2 << 16) + (i1 << 8) + i0; + } + + public long readLong() + { + long i7 = ab[aboffset++]& 0xff; + long i6 = ab[aboffset++]& 0xff; + long i5 = ab[aboffset++]& 0xff; + long i4 = ab[aboffset++]& 0xff; + long i3 = ab[aboffset++]& 0xff; + long i2 = ab[aboffset++]& 0xff; + long i1 = ab[aboffset++]& 0xff; + long i0 = ab[aboffset++]& 0xff; + return (i7 << 56) + (i6 << 48) + (i5 << 40) + (i4 << 32) + (i3 << 24) + (i2 << 16) + (i1 << 8) + i0; + } + + public boolean readBoolean() + { + int i0 = ab[aboffset++]& 0xff; + return i0 != 0; + } + + public byte readByte() + { + int i0 = ab[aboffset++] & 0xff; + return (byte)(i0); + } + + public short readShort() + { + int i1 = ab[aboffset++] & 0xff; + int i0 = ab[aboffset++] & 0xff; + return (short)( (i1 << 8) | i0); + } +} diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/ByteDataWriter.java b/brouter-mapaccess/src/main/java/btools/mapaccess/ByteDataWriter.java new file mode 100644 index 0000000..9c6636b --- /dev/null +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/ByteDataWriter.java @@ -0,0 +1,54 @@ +/** + * fast data-reading from a byte-array + * + * @author ab + */ +package btools.mapaccess; + + +final class ByteDataWriter +{ + private byte[] ab; + private int aboffset; + + public ByteDataWriter( byte[] byteArray ) + { + ab = byteArray; + } + + public void writeInt( int v ) + { + ab[aboffset++] = (byte)( (v >> 24) & 0xff ); + ab[aboffset++] = (byte)( (v >> 16) & 0xff ); + ab[aboffset++] = (byte)( (v >> 8) & 0xff ); + ab[aboffset++] = (byte)( (v ) & 0xff ); + } + + public void writeLong( long v ) + { + ab[aboffset++] = (byte)( (v >> 56) & 0xff ); + ab[aboffset++] = (byte)( (v >> 48) & 0xff ); + ab[aboffset++] = (byte)( (v >> 40) & 0xff ); + ab[aboffset++] = (byte)( (v >> 32) & 0xff ); + ab[aboffset++] = (byte)( (v >> 24) & 0xff ); + ab[aboffset++] = (byte)( (v >> 16) & 0xff ); + ab[aboffset++] = (byte)( (v >> 8) & 0xff ); + ab[aboffset++] = (byte)( (v ) & 0xff ); + } + + public void writeBoolean( boolean v) + { + ab[aboffset++] = (byte)( v ? 1 : 0 ); + } + + public void writeByte( int v ) + { + ab[aboffset++] = (byte)( (v ) & 0xff ); + } + + public void writeShort( int v ) + { + ab[aboffset++] = (byte)( (v >> 8) & 0xff ); + ab[aboffset++] = (byte)( (v ) & 0xff ); + } +} diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/DistanceChecker.java b/brouter-mapaccess/src/main/java/btools/mapaccess/DistanceChecker.java new file mode 100644 index 0000000..4966926 --- /dev/null +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/DistanceChecker.java @@ -0,0 +1,16 @@ +/** + * Container for routig configs + * + * @author ab + */ +package btools.mapaccess; + +public interface DistanceChecker +{ + /** + * Checks whether the given path is within a maximum distance + * known to the distance checker + * @return true if close enough + */ + boolean isWithinRadius( int ilon0, int ilat0, OsmTransferNode firstTransfer, int ilon1, int ilat1 ); +} diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/MicroCache.java b/brouter-mapaccess/src/main/java/btools/mapaccess/MicroCache.java new file mode 100644 index 0000000..eb6a752 --- /dev/null +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/MicroCache.java @@ -0,0 +1,232 @@ +/** + * cache for a single square + * + * @author ab + */ +package btools.mapaccess; + +import java.util.*; +import java.io.*; + +final class MicroCache +{ + private long[] faid; + private int[] fapos; + private int size = 0; + private int delcount = 0; + private int delbytes = 0; + private int p2size; // next power of 2 of size + + // the object parsing position and length + private byte[] ab; + private int aboffset; + private int ablength; + + public MicroCache( OsmFile segfile, int lonIdx80, int latIdx80, byte[] iobuffer ) throws Exception + { + int lonDegree = lonIdx80/80; + int latDegree = latIdx80/80; + + int lonIdxBase = (lonIdx80/5)*62500 + 31250; + int latIdxBase = (latIdx80/5)*62500 + 31250; + + int subIdx = (latIdx80-80*latDegree)*80 + (lonIdx80-80*lonDegree); + + try + { + ab = iobuffer; + int asize = segfile.getDataInputForSubIdx(subIdx, ab); + if ( asize == 0 ) + { + return; + } + if ( asize > iobuffer.length ) + { + ab = new byte[asize]; + asize = segfile.getDataInputForSubIdx(subIdx, ab); + } + aboffset = 0; + size = readInt(); + + // new array with only net data + byte[] nab = new byte[asize - 4 - size*8]; + int noffset = 0; + faid = new long[size]; + fapos = new int[size]; + p2size = 0x40000000; + while( p2size > size ) p2size >>= 1; + + for(int i = 0; i 0 ) + { + int nn = n + offset; + if ( nn < size && a[nn] <= id ) + { + n = nn; + } + offset >>= 1; + } + if ( a[n] == id ) + { + if ( ( fapos[n] & 0x80000000 ) == 0 ) + { + aboffset = fapos[n]; + ablength = ( n+1 < size ? fapos[n+1] & 0x7fffffff : ab.length ) - aboffset; + fapos[n] |= 0x80000000; // mark deleted + delbytes+= ablength; + delcount++; + return true; + } + else + { + throw new RuntimeException( "MicroCache: node already consumed: id=" + id ); + } + } + return false; + } + + public void fillNode( OsmNode node, OsmNodesMap nodesMap, DistanceChecker dc ) + { + long id = node.getIdFromPos(); + if ( getAndClear( id ) ) + { + node.parseNodeBody( this, ablength, nodesMap, dc ); + } + + if ( delcount > size / 2 ) // garbage collection + { + int nsize = size - delcount; + if ( nsize == 0 ) + { + faid = null; + fapos = null; + } + else + { + long[] nfaid = new long[nsize]; + int[] nfapos = new int[nsize]; + int idx = 0; + + byte[] nab = new byte[ab.length - delbytes]; + int nab_off = 0; + for( int i=0; i size ) p2size >>= 1; + } + } + + public List getPositions( OsmNodesMap nodesMap ) + { + ArrayList positions = new ArrayList(); + + for( int i=0; i fileCache; + private HashMap indexCache; + private byte[] iobuffer; + + private OsmFile[][] fileRows = new OsmFile[180][]; + private ArrayList segmentList = new ArrayList(); + + public DistanceChecker distanceChecker; + + public boolean oom_carsubset_hint = false; + + public NodesCache( String segmentDir, OsmNodesMap nodesMap, int lookupVersion, boolean carMode, NodesCache oldCache ) + { + this.segmentDir = segmentDir; + this.nodesMap = nodesMap; + this.lookupVersion = lookupVersion; + this.carMode = carMode; + + if ( oldCache != null ) + { + fileCache = oldCache.fileCache; + indexCache = oldCache.indexCache; + iobuffer = oldCache.iobuffer; + oom_carsubset_hint = oldCache.oom_carsubset_hint; + } + else + { + fileCache = new HashMap(4); + indexCache = new HashMap(4); + iobuffer = new byte[65636]; + } + } + + public int loadSegmentFor( int ilon, int ilat ) + { + MicroCache mc = getSegmentFor( ilon, ilat ); + return mc == null ? 0 : mc.getSize(); + } + + public MicroCache getSegmentFor( int ilon, int ilat ) + { + try + { + int lonIdx80 = ilon/12500; + int latIdx80 = ilat/12500; + int lonDegree = lonIdx80/80; + int latDegree = latIdx80/80; + OsmFile osmf = null; + OsmFile[] fileRow = fileRows[latDegree]; + int ndegrees = fileRow == null ? 0 : fileRow.length; + for( int i=0; i> 48); + if ( readVersion != lookupVersion ) + { + throw new IllegalArgumentException( "lookup version mismatch (old rd5?) lookups.dat=" + + lookupVersion + " " + f. getAbsolutePath() + "=" + readVersion ); + } + fileIndex[i] = lv & 0xffffffffffffL; + } + indexCache.put( filenameBase, fileIndex ); + } + } + RandomAccessFile ra = fileCache.get( filenameBase ); + long startPos = 0L; + if ( ra != null ) + { + long[] index = indexCache.get( filenameBase ); + startPos = tileIndex > 0 ? index[ tileIndex-1 ] : 200L; + if ( startPos == index[ tileIndex] ) ra = null; + } + OsmFile osmf = new OsmFile( ra, startPos, iobuffer ); + osmf.lonDegree = lonDegree; + osmf.latDegree = latDegree; + osmf.filename = currentFileName; + return osmf; + } + + public List getAllNodes() + { + List all = new ArrayList(); + for( MicroCache segment : segmentList ) + { + List positions = segment.getPositions( nodesMap ); + all.addAll( positions ); + } + return all; + } + + + public void close() + { + for( RandomAccessFile f: fileCache.values() ) + { + try + { + f.close(); + } + catch( IOException ioe ) + { + // ignore + } + } + } +} diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/NodesList.java b/brouter-mapaccess/src/main/java/btools/mapaccess/NodesList.java new file mode 100644 index 0000000..1ab0752 --- /dev/null +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/NodesList.java @@ -0,0 +1,14 @@ +/** + * Container for link between two Osm nodes + * + * @author ab + */ +package btools.mapaccess; + +import java.util.*; + +final class NodesList +{ + public OsmNode node; + public NodesList next; +} diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/OsmFile.java b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmFile.java new file mode 100644 index 0000000..4327e09 --- /dev/null +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmFile.java @@ -0,0 +1,67 @@ +/** + * cache for a single square + * + * @author ab + */ +package btools.mapaccess; + +import java.io.IOException; +import java.io.RandomAccessFile; + +final class OsmFile +{ + private RandomAccessFile is = null; + private long fileOffset; + + private int[] posIdx; + public MicroCache[] microCaches; + + public int lonDegree; + public int latDegree; + + public String filename; + + public OsmFile( RandomAccessFile rafile, long startPos, byte[] iobuffer ) throws Exception + { + fileOffset = startPos; + if ( rafile != null ) + { + is = rafile; + posIdx = new int[6400]; + microCaches = new MicroCache[6400]; + is.seek( fileOffset ); + is.readFully( iobuffer, 0, 25600 ); + ByteDataReader dis = new ByteDataReader( iobuffer ); + for( int i=0; i<6400; i++ ) + { + posIdx[i] = dis.readInt(); + } + } + } + + private int getPosIdx( int idx ) + { + return idx == -1 ? 25600 : posIdx[idx]; + } + + public int getDataInputForSubIdx( int subIdx, byte[] iobuffer ) throws Exception + { + int startPos = getPosIdx(subIdx-1); + int endPos = getPosIdx(subIdx); + int size = endPos-startPos; + if ( size > 0 ) + { + is.seek( fileOffset + startPos ); + if ( size <= iobuffer.length ) + { + is.readFully( iobuffer ); + } + } + return size; + } + + public void close() + { + try { is.close(); } catch( IOException e ) { throw new RuntimeException( e ); } + } +} diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/OsmLink.java b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmLink.java new file mode 100644 index 0000000..70d92bd --- /dev/null +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmLink.java @@ -0,0 +1,53 @@ +/** + * Container for link between two Osm nodes + * + * @author ab + */ +package btools.mapaccess; + +import java.util.*; + +public final class OsmLink +{ + /** + * The description bitmap is mainly the way description + * used to calculate the costfactor + */ + public long descriptionBitmap; + + /** + * The target is either the next link or the target node + */ + public OsmNode targetNode; + + /** + * The origin position + */ + public int ilatOrigin; + public int ilonOrigin; + + public OsmLink next; + + public byte[] firsttransferBytes; + + public OsmTransferNode decodeFirsttransfer() + { + return firsttransferBytes == null ? null : OsmTransferNode.decode( firsttransferBytes ); + } + + public void encodeFirsttransfer( OsmTransferNode firsttransfer ) + { + if ( firsttransfer == null ) firsttransferBytes = null; + else firsttransferBytes = OsmTransferNode.encode( firsttransfer ); + } + + public boolean counterLinkWritten; + + public OsmLinkHolder firstlinkholder = null; + + public void addLinkHolder( OsmLinkHolder holder ) + { + if ( firstlinkholder != null ) { holder.setNextForLink( firstlinkholder ); } + firstlinkholder = holder; + } +} diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/OsmLinkHolder.java b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmLinkHolder.java new file mode 100644 index 0000000..da572d3 --- /dev/null +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmLinkHolder.java @@ -0,0 +1,13 @@ +/** + * Container for routig configs + * + * @author ab + */ +package btools.mapaccess; + +public interface OsmLinkHolder +{ + void setNextForLink( OsmLinkHolder holder ); + + OsmLinkHolder getNextForLink(); +} diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/OsmNode.java b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmNode.java new file mode 100644 index 0000000..a97856b --- /dev/null +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmNode.java @@ -0,0 +1,397 @@ +/** + * Container for an osm node + * + * @author ab + */ +package btools.mapaccess; + + + +public class OsmNode implements OsmPos, Comparable +{ + private static final long serialVersionUID = -4166565134085275556L; + + public static final int EXTERNAL_BITMASK = 0x80; + public static final int FIRSTFORWAY_BITMASK = 0x40; + public static final int TRANSFERNODE_BITMASK = 0x20; + public static final int WRITEDESC_BITMASK = 0x10; + public static final int SKIPDETAILS_BITMASK = 0x08; + public static final int NODEDESC_BITMASK = 0x04; + + public OsmNode() + { + } + + public OsmNode( int ilon, int ilat ) + { + this.ilon = ilon; + this.ilat = ilat; + } + + public OsmNode( long id ) + { + ilon = (int)(id >> 32); + ilat = (int)(id & 0xffffffff); + } + + /** + * The latitude + */ + public int ilat; + + /** + * The longitude + */ + public int ilon; + + /** + * The elevation + */ + public short selev; + + public long nodeDescription; + + // interface OsmPos + public int getILat() + { + return ilat; + } + + public int getILon() + { + return ilon; + } + + public short getSElev() + { + return selev; + } + + public double getElev() + { + return selev / 4.; + } + + /** + * Whether there's a traffic signal + */ + + /** + * The links to other nodes + */ + public OsmLink firstlink = null; + + public OsmLink firstreverse = null; + + // whether this node is completed and registerd for map-removal + public boolean completed; + + public boolean wasProcessed; + public int maxcost; // maximum cost to consider for that node + + public void addLink( OsmLink link ) + { + if ( firstlink != null ) link.next = firstlink; + firstlink = link; + } + + public int calcDistance( OsmPos p ) + { + double l = (ilat-90000000) * 0.00000001234134; + double l2 = l*l; + double l4 = l2*l2; + double coslat = 1.- l2 + l4 / 6.; + + double dlat = (ilat - p.getILat() )/1000000.; + double dlon = (ilon - p.getILon() )/1000000. * coslat; + double d = Math.sqrt( dlat*dlat + dlon*dlon ) * (6378000. / 57.3); + return (int)(d + 1.0 ); + } + + + public void parseNodeBody( MicroCache is, int bodySize, OsmNodesMap hollowNodes, DistanceChecker dc ) + { + selev = is.readShort(); + bodySize -= 2; + + OsmLink lastlink = null; + + int lonIdx = ilon/62500; + int latIdx = ilat/62500; + + while( bodySize > 0 ) + { + OsmLink link = new OsmLink(); + OsmTransferNode firstTransferNode = null; + OsmTransferNode lastTransferNode = null; + int linklon; + int linklat; + long description = 0L; + for(;;) + { + int bitField = is.readByte(); + bodySize -= 1; + if ( (bitField & EXTERNAL_BITMASK) != 0 ) + { + // full position for external target + bodySize -= 8; + linklon = is.readInt(); + linklat = is.readInt(); + } + else + { + // reduced position for internal target + bodySize -= 4; + linklon = is.readShort(); + linklat = is.readShort(); + linklon += lonIdx*62500 + 31250; + linklat += latIdx*62500 + 31250; + } + if ( (bitField & WRITEDESC_BITMASK ) != 0 ) + { + description = is.readLong(); + bodySize -= 8; + } + if ( (bitField & NODEDESC_BITMASK ) != 0 ) + { + nodeDescription = is.readLong(); + bodySize -= 8; + } + if ( (bitField & SKIPDETAILS_BITMASK ) != 0 ) + { + link.counterLinkWritten = true; + } + boolean isTransfer = (bitField & TRANSFERNODE_BITMASK ) != 0; + if ( isTransfer ) + { + OsmTransferNode trans = new OsmTransferNode(); + trans.ilon = linklon; + trans.ilat = linklat; + trans.descriptionBitmap = description; + bodySize -= 2; + trans.selev = is.readShort(); + if ( lastTransferNode == null ) + { + firstTransferNode = trans; + } + else + { + lastTransferNode.next = trans; + } + lastTransferNode = trans; + } + else + { + link.descriptionBitmap = description; + break; + } + } + + // performance shortcut: ignore link if out of reach + if ( dc != null && !link.counterLinkWritten ) + { + if ( !dc.isWithinRadius( ilon, ilat, firstTransferNode, linklon, linklat ) ) + { + continue; + } + } + + if ( linklon == ilon && linklat == ilat ) + { + continue; // skip self-ref + } + + if ( lastlink == null ) + { + firstlink = link; + } + else + { + lastlink.next = link; + } + lastlink = link; + + + long targetNodeId = ((long)linklon)<<32 | linklat; + OsmNode tn = hollowNodes.get( targetNodeId ); // target node + + if ( tn == null ) + { + // node not yet known, create a hollow proxy + tn = new OsmNode(linklon, linklat); + tn.setHollow(); + hollowNodes.put( targetNodeId, tn ); + } + else + { + if ( !( tn.isHollow() || tn.hasHollowLinks() ) ) + { + hollowNodes.registerCompletedNode( tn ); + } + } + link.targetNode = tn; + + link.encodeFirsttransfer(firstTransferNode); + + // compute the reverse link + if ( !link.counterLinkWritten ) + { + OsmLink rlink = new OsmLink(); + long rerverseLinkBitmap = link.descriptionBitmap ^ 1L; + + rlink.ilonOrigin = tn.ilon; + rlink.ilatOrigin = tn.ilat; + rlink.targetNode = this; + rlink.descriptionBitmap = rerverseLinkBitmap; // default for no transfer-nodes + OsmTransferNode previous = null; + OsmTransferNode rtrans = null; + for( OsmTransferNode trans = firstTransferNode; trans != null; trans = trans.next ) + { + long rerverseTransBitmap = trans.descriptionBitmap ^ 1L; + if ( previous == null ) + { + rlink.descriptionBitmap = rerverseTransBitmap; + } + else + { + previous.descriptionBitmap = rerverseTransBitmap; + } + rtrans = new OsmTransferNode(); + rtrans.ilon = trans.ilon; + rtrans.ilat = trans.ilat; + rtrans.selev = trans.selev; + rtrans.next = previous; + rtrans.descriptionBitmap = rerverseLinkBitmap; + previous = rtrans; + } + rlink.encodeFirsttransfer(rtrans); + rlink.next = firstreverse; + firstreverse = rlink; + } + + } + + if ( !hasHollowLinks() ) + { + hollowNodes.registerCompletedNode( this ); + } + } + + public boolean isHollow() + { + return selev == -12345; + } + + public void setHollow() + { + selev = -12345; + } + + public long getIdFromPos() + { + return ((long)ilon)<<32 | ilat; + } + + public boolean hasHollowLinks() + { + for( OsmLink link = firstlink; link != null; link = link.next ) + { + if ( link.targetNode.isHollow() ) return true; + } + return false; + } + + + public int linkCnt() + { + int cnt = 0; + + for( OsmLink link = firstlink; link != null; link = link.next ) + { + cnt++; + } + return cnt; + } + + public void unlinkLink( OsmLink link ) + { + if ( link == firstlink ) + { + firstlink = link.next; + return; + } + for( OsmLink l = firstlink; l != null; l = l.next ) + { + if ( l.next == link ) + { + l.next = link.next; + return; + } + } + } + + /** + * Compares two OsmNodes for position ordering. + * + * @return -1,0,1 depending an comparson result + */ + public int compareTo( Object o ) + { + OsmNode n = (OsmNode)o; + long id1 = getIdFromPos(); + long id2 = n.getIdFromPos(); + if ( id1 < id2 ) return -1; + if ( id1 > id2 ) return 1; + return 0; + } + + /** + * @return if equals in the sense of compareTo == 0 + */ + public boolean equals( Object o ) + { + return compareTo( o ) == 0; + } + + // mark the link to the given node as written, + // don't want to write the counter-direction + // in full details + public void markLinkWritten( OsmNode t ) + { + for( OsmLink link = firstlink; link != null; link = link.next ) + { + if ( link.targetNode == t) link.counterLinkWritten = true; + } + } + + public OsmLink getReverseLink( int lon, int lat ) + { + for( OsmLink rlink = firstreverse; rlink != null; rlink = rlink.next ) + { + if ( rlink.ilonOrigin == lon && rlink.ilatOrigin == lat ) + { + unlinkRLink( rlink ); + return rlink; + } + } + return null; + } + + public void unlinkRLink( OsmLink rlink ) + { + if ( rlink == firstreverse ) + { + firstreverse = rlink.next; + return; + } + for( OsmLink l = firstreverse; l != null; l = l.next ) + { + if ( l.next == rlink ) + { + l.next = rlink.next; + return; + } + } + } + +} diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/OsmNodesMap.java b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmNodesMap.java new file mode 100644 index 0000000..d9db767 --- /dev/null +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmNodesMap.java @@ -0,0 +1,109 @@ +/** + * Container for link between two Osm nodes + * + * @author ab + */ +package btools.mapaccess; + +import java.util.*; + +public final class OsmNodesMap +{ + private HashMap hmap = new HashMap(); + + private NodesList completedNodes = null; + + /** + * Get a node from the map + * @return the node for the given id if exist, else null + */ + public OsmNode get( long id ) + { + return hmap.get( new Long( id ) ); + } + + + public void remove( long id ) + { + hmap.remove( new Long( id ) ); + } + + public void removeCompletedNodes() + { + for( NodesList le = completedNodes; le != null; le = le.next ) + { + remove( le.node.getIdFromPos() ); + } + completedNodes = null; + } + + public void registerCompletedNode( OsmNode n ) + { + if ( n.completed ) return; + n.completed = true; + NodesList le = new NodesList(); + le.node = n; + if ( completedNodes != null ) le.next = completedNodes; + completedNodes = le; + } + + /** + * Put a node into the map + * @return the previous node if that id existed, else null + */ + public OsmNode put( long id, OsmNode node ) + { + return hmap.put( new Long( id ), node ); + } + + /** + * Return the internal list. + * A reference is returned, not a copy- + * @return the nodes list + */ + public Collection nodes() + { + return hmap.values(); + } + + /** + * @return the number of nodes in that map + */ + public int size() + { + return hmap.size(); + } + + /** + * cleanup the map by removing the nodes + * with no hollow issues + */ + + private int dontCareCount = 0; + + public void removeCompleteNodes() + { + if ( ++dontCareCount < 5 ) return; + dontCareCount = 0; + + ArrayList delNodes = new ArrayList(); + + for( OsmNode n : hmap.values() ) + { + if ( n.isHollow() || n.hasHollowLinks() ) + { + continue; + } + delNodes.add( n ); + } + + if ( delNodes.size() > 0 ) + { +// System.out.println( "removing " + delNodes.size() + " nodes" ); + for( OsmNode n : delNodes ) + { + hmap.remove( new Long( n.getIdFromPos() ) ); + } + } + } +} diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/OsmPos.java b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmPos.java new file mode 100644 index 0000000..e6f48cb --- /dev/null +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmPos.java @@ -0,0 +1,23 @@ +/** + * Interface for a position (OsmNode or OsmPath) + * + * @author ab + */ +package btools.mapaccess; + + +public interface OsmPos +{ + public int getILat(); + + public int getILon(); + + public short getSElev(); + + public double getElev(); + + public int calcDistance( OsmPos p ); + + public long getIdFromPos(); + +} diff --git a/brouter-mapaccess/src/main/java/btools/mapaccess/OsmTransferNode.java b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmTransferNode.java new file mode 100644 index 0000000..a4744ba --- /dev/null +++ b/brouter-mapaccess/src/main/java/btools/mapaccess/OsmTransferNode.java @@ -0,0 +1,144 @@ +/** + * Container for link between two Osm nodes + * + * @author ab + */ +package btools.mapaccess; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; + + +public final class OsmTransferNode +{ + /** + * The description bitmap is mainly the way description + * used to calculate the costfactor + */ + public long descriptionBitmap; + + public OsmTransferNode next; + + public int ilon; + public int ilat; + public short selev; + + private static final int BIT_DESC = 1; + private static final int BIT_ILONHIGH = 2; + private static final int BIT_ILATHIGH = 4; + private static final int BIT_STOP = 8; + + // encode this transfer-node into a byte array + public static byte[] encode( OsmTransferNode tn ) + { + long currentDesc = 0; + int currentILonHigh = 0; + int currentILatHigh = 0; + OsmTransferNode n = tn; + + // first loop to calc size + int size = 1; // stop-bit + + while( n != null ) + { + if( n.descriptionBitmap != currentDesc ) + { + size += 8; + currentDesc = n.descriptionBitmap; + } + if( ( n.ilon >> 16 ) != currentILonHigh ) + { + size += 2; + currentILonHigh = n.ilon >> 16; + } + if( (n.ilat >> 16) != currentILatHigh ) + { + size += 2; + currentILatHigh = n.ilat >> 16; + } + size += 7; + n = n.next; + } + + byte[] ab = new byte[size]; + ByteDataWriter os = new ByteDataWriter( ab ); + + currentDesc = 0; + currentILonHigh = 0; + currentILatHigh = 0; + n = tn; + while( n != null ) + { + int mode = 0; + if( n.descriptionBitmap != currentDesc ) + { + mode |= BIT_DESC; + currentDesc = n.descriptionBitmap; + } + if( ( n.ilon >> 16 ) != currentILonHigh ) + { + mode |= BIT_ILONHIGH; + currentILonHigh = n.ilon >> 16; + } + if( (n.ilat >> 16) != currentILatHigh ) + { + mode |= BIT_ILATHIGH; + currentILatHigh = n.ilat >> 16; + } + os.writeByte( mode); + if ( (mode & BIT_DESC) != 0 ) os.writeLong( currentDesc ); + if ( (mode & BIT_ILONHIGH) != 0 ) os.writeShort( currentILonHigh ); + if ( (mode & BIT_ILATHIGH) != 0 ) os.writeShort( currentILatHigh ); + os.writeShort( n.ilon ); + os.writeShort( n.ilat ); + os.writeShort( n.selev ); + n = n.next; + } + os.writeByte( BIT_STOP ); + return ab; + } + + // decode a transfer-node from a byte array + public static OsmTransferNode decode( byte[] ab ) + { + ByteDataReader is = new ByteDataReader( ab ); + + OsmTransferNode firstNode = null; + OsmTransferNode lastNode = null; + long currentDesc = 0; + int currentILonHigh = 0; + int currentILatHigh = 0; + for(;;) + { + byte mode = is.readByte(); + if ( (mode & BIT_STOP ) != 0 ) break; + + OsmTransferNode n = new OsmTransferNode(); + if ( (mode & BIT_DESC) != 0 ) currentDesc = is.readLong(); + if ( (mode & BIT_ILONHIGH) != 0 ) currentILonHigh = is.readShort(); + if ( (mode & BIT_ILATHIGH) != 0 ) currentILatHigh = is.readShort(); + n.descriptionBitmap = currentDesc; + int ilon = is.readShort() & 0xffff; ilon |= currentILonHigh << 16; + int ilat = is.readShort() & 0xffff; ilat |= currentILatHigh << 16; + n.ilon = ilon; + n.ilat = ilat; + n.selev = is.readShort(); + + if ( ilon != n.ilon ) System.out.println( "ilon=" + ilon + " n.ilon=" + n.ilon ); + if ( ilat != n.ilat ) System.out.println( "ilat=" + ilat + " n.ilat=" + n.ilat ); + + if ( lastNode != null ) + { + lastNode.next = n; + } + else + { + firstNode = n; + } + lastNode = n; + } + return firstNode; + } + +} diff --git a/brouter-routing-app/AndroidManifest.xml b/brouter-routing-app/AndroidManifest.xml new file mode 100644 index 0000000..1908474 --- /dev/null +++ b/brouter-routing-app/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/brouter-routing-app/classpath b/brouter-routing-app/classpath new file mode 100644 index 0000000..a662f00 --- /dev/null +++ b/brouter-routing-app/classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/brouter-routing-app/gen/btools/routingapp/BuildConfig.java b/brouter-routing-app/gen/btools/routingapp/BuildConfig.java new file mode 100644 index 0000000..ba033d3 --- /dev/null +++ b/brouter-routing-app/gen/btools/routingapp/BuildConfig.java @@ -0,0 +1,5 @@ +package btools.routingapp; + +public final class BuildConfig { + public static final boolean DEBUG = true; +} diff --git a/brouter-routing-app/gen/btools/routingapp/IBRouterService.java b/brouter-routing-app/gen/btools/routingapp/IBRouterService.java new file mode 100644 index 0000000..93ec321 --- /dev/null +++ b/brouter-routing-app/gen/btools/routingapp/IBRouterService.java @@ -0,0 +1,136 @@ +/* + * This file is auto-generated. DO NOT MODIFY. + * Original file: C:\\brouter\\brouter-routing-app\\src\\main\\java\\btools\\routingapp\\IBRouterService.aidl + */ +package btools.routingapp; +public interface IBRouterService extends android.os.IInterface +{ +/** Local-side IPC implementation stub class. */ +public static abstract class Stub extends android.os.Binder implements btools.routingapp.IBRouterService +{ +private static final java.lang.String DESCRIPTOR = "btools.routingapp.IBRouterService"; +/** Construct the stub at attach it to the interface. */ +public Stub() +{ +this.attachInterface(this, DESCRIPTOR); +} +/** + * Cast an IBinder object into an btools.routingapp.IBRouterService interface, + * generating a proxy if needed. + */ +public static btools.routingapp.IBRouterService asInterface(android.os.IBinder obj) +{ +if ((obj==null)) { +return null; +} +android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); +if (((iin!=null)&&(iin instanceof btools.routingapp.IBRouterService))) { +return ((btools.routingapp.IBRouterService)iin); +} +return new btools.routingapp.IBRouterService.Stub.Proxy(obj); +} +@Override public android.os.IBinder asBinder() +{ +return this; +} +@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException +{ +switch (code) +{ +case INTERFACE_TRANSACTION: +{ +reply.writeString(DESCRIPTOR); +return true; +} +case TRANSACTION_getTrackFromParams: +{ +data.enforceInterface(DESCRIPTOR); +android.os.Bundle _arg0; +if ((0!=data.readInt())) { +_arg0 = android.os.Bundle.CREATOR.createFromParcel(data); +} +else { +_arg0 = null; +} +java.lang.String _result = this.getTrackFromParams(_arg0); +reply.writeNoException(); +reply.writeString(_result); +return true; +} +} +return super.onTransact(code, data, reply, flags); +} +private static class Proxy implements btools.routingapp.IBRouterService +{ +private android.os.IBinder mRemote; +Proxy(android.os.IBinder remote) +{ +mRemote = remote; +} +@Override public android.os.IBinder asBinder() +{ +return mRemote; +} +public java.lang.String getInterfaceDescriptor() +{ +return DESCRIPTOR; +} +//param params--> Map of params: +// "pathToFileResult"-->String with the path to where the result must be saved, including file name and extension +// -->if null, the track is passed via the return argument +// "maxRunningTime"-->String with a number of seconds for the routing timeout, default = 60 +// "trackFormat"-->[kml|gpx] default = gpx +// "lats"-->double[] array of latitudes; 2 values at least. +// "lons"-->double[] array of longitudes; 2 values at least. +// "nogoLats"-->double[] array of nogo latitudes; may be null. +// "nogoLons"-->double[] array of nogo longitudes; may be null. +// "nogoRadi"-->double[] array of nogo radius in meters; may be null. +// "fast"-->[0|1] +// "v"-->[motorcar|bicycle|foot] +//return null if all ok and no path given, the track if ok and path given, an error message if it was wrong +//call in a background thread, heavy task! + +@Override public java.lang.String getTrackFromParams(android.os.Bundle params) throws android.os.RemoteException +{ +android.os.Parcel _data = android.os.Parcel.obtain(); +android.os.Parcel _reply = android.os.Parcel.obtain(); +java.lang.String _result; +try { +_data.writeInterfaceToken(DESCRIPTOR); +if ((params!=null)) { +_data.writeInt(1); +params.writeToParcel(_data, 0); +} +else { +_data.writeInt(0); +} +mRemote.transact(Stub.TRANSACTION_getTrackFromParams, _data, _reply, 0); +_reply.readException(); +_result = _reply.readString(); +} +finally { +_reply.recycle(); +_data.recycle(); +} +return _result; +} +} +static final int TRANSACTION_getTrackFromParams = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); +} +//param params--> Map of params: +// "pathToFileResult"-->String with the path to where the result must be saved, including file name and extension +// -->if null, the track is passed via the return argument +// "maxRunningTime"-->String with a number of seconds for the routing timeout, default = 60 +// "trackFormat"-->[kml|gpx] default = gpx +// "lats"-->double[] array of latitudes; 2 values at least. +// "lons"-->double[] array of longitudes; 2 values at least. +// "nogoLats"-->double[] array of nogo latitudes; may be null. +// "nogoLons"-->double[] array of nogo longitudes; may be null. +// "nogoRadi"-->double[] array of nogo radius in meters; may be null. +// "fast"-->[0|1] +// "v"-->[motorcar|bicycle|foot] +//return null if all ok and no path given, the track if ok and path given, an error message if it was wrong +//call in a background thread, heavy task! + +public java.lang.String getTrackFromParams(android.os.Bundle params) throws android.os.RemoteException; +} diff --git a/brouter-routing-app/gen/btools/routingapp/R.java b/brouter-routing-app/gen/btools/routingapp/R.java new file mode 100644 index 0000000..cf5a5e4 --- /dev/null +++ b/brouter-routing-app/gen/btools/routingapp/R.java @@ -0,0 +1,22 @@ +/* AUTO-GENERATED FILE. DO NOT MODIFY. + * + * This class was automatically generated by the + * aapt tool from the resource data it found. It + * should not be modified by hand. + */ + +package btools.routingapp; + +public final class R { + public static final class attr { + } + public static final class drawable { + public static final int icon=0x7f020000; + } + public static final class layout { + public static final int main=0x7f030000; + } + public static final class string { + public static final int app_name=0x7f040000; + } +} diff --git a/brouter-routing-app/pom.xml b/brouter-routing-app/pom.xml new file mode 100644 index 0000000..b72774c --- /dev/null +++ b/brouter-routing-app/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + org.btools + brouter + 0.98 + ../pom.xml + + brouter-routing-app + apk + + + + com.google.android + android + 4.1.1.4 + provided + + + org.btools + brouter-core + ${project.version} + + + + + + + com.jayway.maven.plugins.android.generation2 + android-maven-plugin + + + + diff --git a/brouter-routing-app/project b/brouter-routing-app/project new file mode 100644 index 0000000..951383c --- /dev/null +++ b/brouter-routing-app/project @@ -0,0 +1,33 @@ + + + AccelerometerPlay + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/brouter-routing-app/project.properties b/brouter-routing-app/project.properties new file mode 100644 index 0000000..f049142 --- /dev/null +++ b/brouter-routing-app/project.properties @@ -0,0 +1,11 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "ant.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-10 diff --git a/brouter-routing-app/res/drawable-hdpi/icon.png b/brouter-routing-app/res/drawable-hdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5d890ca210faf477c01cf85fb9a843e8edb81154 GIT binary patch literal 11374 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 u(_4Jj83jmFX~$P=aMxE-m7}^ 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 P00000NkvXXu0mjfA9WB~ literal 0 HcmV?d00001 diff --git a/brouter-routing-app/res/drawable-mdpi/icon.png b/brouter-routing-app/res/drawable-mdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..88e31c15e3712aa04c67f428c2e76892f2813b32 GIT binary patch literal 5479 zcmV-t6`1OYP)7Jf_B`JzSGn6HgmKDbq zEWm~ZM=@YSNbI*54v@e|fW%3lD9eLlJGKKku^;kcM22P4mLrLzIif^yINWAsPw&g^ zzWesye?8|^9%dv_@b^}QbLxDks#B;c@}(bq z5K-%99y((Htq3zfOM{tU7X8Y6yE8Z*O{cbnoo)~Wahw|9XjB^2Vmth?r_<42?fePF z#QmtYowmDb3;^7z0HMO4BZQ#FFpQS*3j96T)Iu!@6SbUfjVVxuIzvFu%%*W_=t+Mt z3d4vCp|vK2a?UL90l-St+TL`r6MfWLkd3)c3O|%BYCWI7US)a0IAumM*i6m&!DOH_ zNYgBZAG=I|TyoFVj0$K4%qTcQYCVh4F@OM2tIT4~jR+tD0^GJ4g4W73ja=F^$o+gU z?iNiemhj`?3#B~Io_{1T!dmSz$w8xR%Xo|`9Sn#4{#Xb>Dao<&DS9@9aVt4`bUx3D zpcMe1O0zhv`{qU$mx@SHrh|io+8S-nlQ4|N1gs!ng7=%ZL`M_7lD{emQ$3(h&mQkJnFH zO&}BzLt-zM)WMKkSzxH8wv1^;;!&zKGD>HZ`AShrJ=bGuViS`CS>?I&#FyONgfNp+ zA2D)q(!M**9RxN8}C*IZq!?HzY_4+j7+3PN|W3Xo_SUtVgCL%qA-%KHTe0)P~N%=7AL zXYFCQam6{0gb*MA8c;hzdnUF+yOAT58O4H?4MWgS{L8OP-<@8&SRamDzcsF2zIU`a z)kjgb6Vb;xv{b#|du2q;Ct-Yd>u?$*jFH`=9--vQndLZaL0-fe31>1(qSZPl1Q8WM z0Lln6KIUJoh*HHU6G}lTAPfKrWs5~T=*10}DkfaV9(HCYw>DS)>wnvQ^%YgKjT`SL zB{CIwe&Gsu7ke-gnhe5DceuOUysYBb2nm1c{0aaRde`Q+qMz2iR|TAF70+Ne(66^DZ@C) zBAzSfMscoLtM8^CcDjeLN{U{CWJ;Kp(5$KFeZRn-`l4~LJ$JtH`as;Qn0r^P;Ylx* zR$bPgwQ%amR>bPFSb8==RD>CTNC1Ek)G6U+#~CYzM#ca&5Go9Ea~q#HX{S6DBIo_- zpV_xRAhXbJAK^e9jegw@an30eYSs4^>IGZ7O)2;qcKmjWRqPQlM? zI_}S2yngS`{%E=|4~EJ9@T;@h7MGU==Ku{@Znslp_P`+@9Lye1MrK#m%y6+K9F zAv9cndPap*I6x^CuACm_YD-$rFCV{)T5S(c_9kX?d9uiD07A3^0**3drWDmoGt?3& z%NZ(&Qo|y_8}D5FXTPOPlK?q<_9yx;z3A=k>nIeyJFhR4%>F^TMG2-^Y=E__&Lx4G z1R+O2YV_lgaXE7|Ap|v(P}Cj( zpw1NK47A`plK_)(`gd=B@Y3`8KYYHoS(rP0CJjRhh)H6WOK%^&*2Zl>$aKo7K&89k zEXf~MaD@W^HKT0gO-a8XtDrLk0xFZqlu(8$0n{c@K$5AkxH!H;tEG)r$!R7mV}w$Z zxr7@s>=PyeiAB>vjLaY^ooFhMs!+Vq(u+bUr81cUfCNZo=J@t?|EIpw`s+_E{Cup! zn%z7O_t!_mKy@epP;;nbKP4f?I#LM%1WE}L0Dv;3h6kt!C_*4CJyj3JD{gRa*MSTI zt`o0_+}A0~GF4!xwVov^|9}4P=7YWbJ9nzjed-fuE@xB{gCWGgfHMXNj;y}jepR8o z*?EN!YI8qtR4R7kMBxkt0H8=jg3=y~?(NAzph6;Cky}8m5M@7Z0{|ir0GS3edubkz z>mCjwQA`;bjb_`e!+};3fKbe4vy>Y1mE|w~^TuG(zk1Dj|1dv|RT48|@hFWsw>)t# zzA=+i0GOo1?;U;PjibN0H@tbS_+%E0>7znP#Yji_l*GqT=e_X3t>K&Z`fmYclo=)F z0p*V>4a$^I9vaQF;$hv(05xPGA;dHcO%V}?5rT-?Yx(-YF6;Hqz4&6`gZFXHjWI1& zTsMldQlUG#VNS;evwTrJRk-xar+@Qf^-m7s?#=cq%@3o;Jr}hCop{U#LU1eE0st~2 z_0p$}P#TUyxdn`)$_NkuP|Qxycf-w*J~;^~1DWIewxr~hwol8{mPZk6X5TKZ>|S--fzDSK&}nXIm!MH35`@R2LK4@ zGS{M3KkFaM{+ho`2p}p@3J3whP)kBsCMsb9MX~|hnW@GkgUjIN6n|@gc2(%Vek*?N z>A@d=Rf8dtgjtqKQ^r`zc~NK6T*0J;qt1i%Y{NH-y~%^0TVG`g$JWMcOYI*JGJ-VD zxhl_@y69aZ2sg6+NRElk001xpAb^M{3FV-*R61s*L)$m9xXJcs+AG5RVHEdbcHT3Z zU;p~ful|ypg{g`dBQ#CYR05!+G`3{)%qJW^x6yfBj>e@SJu|GxOXvRvzA=?!M#T?3 zG=U^%^HRe4642*90dknMwv5U$uQP4}A^<@x0TlvHQ<}FVCyl+VpFyE6dI}!o8_zhc z-gB*uT*fdQ>nc>-NGBZXKJ4td`ZP4GDwhylktdjeQ)YbiEcOa*!&Q9je zRjda4sb78i5hp!a`FOKa3sm6-1Sc_NsQa^o_5PxpMT(^_RbOV>ldzQDj<9`{>w4y=#c|ont=n8nnJ0vuv)` zisk;{Ad8e&Eou@j`Db`Gsy-rQB556SnUjDBkZ4_1p*`|OT1!R+B1O~$fDnqPJX^fc z>R%}PRhy%eCqyF4Z}sxClwJJxJBPpi#VF2d)yl(%o2F@6*0kxmd$SM#C832z!qEl-Q-V{KbNHv0Kf+T80ts#fUw3T|LlJDT4G5a6G9lG zM$`a6m_a38Efjo{5C9+mfP}GWs)b+}rb%r{jk3c2;b%Ylv-|sp^?EJe9T`vF&iEA(Tu00OoI~xbW5RPevX;LZrH{N+T znMEfmCu6Z)B}D*y-1l(jW_QpE!_|U0nQ7aks0}IwN$jnGwjEr3^_$}}XBK|<_rx#$ zLh87tX*T+UwA*!jJ2&4<(@ZKwDXFh`EptO!$=S-KT5c}K^9^UQY}GjcoH^0dD7Ozg z*DtMf2Epz3_D{@Jv{s{Oz;xpvXr1$y3FQEUaFND=lIn?)D;08+smut6GDS3~UcCEm zqqn6SrPbg0_dEhzvXSW_I88qW}?_HeB5a-ubo`~W0pzJ7D6~3Ve}j` zu*{{sy-6VtjxD96l(N3RP(QI3tlPvTgaD#lH+V}gAI@}x^;XAs*28LB7INJbw1%S1 zKs^ZVd4efX;@kjKh?q&zq!nb!r$&;WB}x1$bZu@uyI6~>_}VQU%#xL40Mn^6ixN(i zsTLZ!L^{Ootar`JySt(7^n5QW^d@ss39b=_B ztJz({%SY5*Fw{{th%s3)ya|dDYSapV6by?X+e)0Bp^@(%nx??_aQ~lii zTh%{#$9?za&hLL&7V{<*2mSr>LNx&x4>54f`0~8dkCwNO_l4-u9$3by;??BdcZ~hF zZ0Q;G z@$|qOEhX}k|NU#+cT3;Ded4$N@XE)3ZS9r6y}fXGU?pnpi|Msn^Xi&k;|5{@>WG9&#qB(f;{=(EX*KH0B#0+{$05 z;mI;|C-FfqrC@+Y%T7<#U1inRH@7aFC}9|q`$q%6vfZ3U=17|g=%fD)T_ItZQU5#m}HLMOAbo=8RS>Ecac|3BvQlt?qT(R{7?VZgDmI0{|EmfeeUvd zlyA(sW@@vQ(%T>QPn2{eXHb>uL@q#I2D%fJ{e7mIh>n=4XV`Ulkps-nqv zMw2zW1c=9jq&+_5xj`T&1Ww|aB!5sI)g$qAi?ABd5``OY=Kl0As_W~cPd`n5 z`nvnp{gZ$2=MS!)F$CvcHFJHXjZwmSp{}?D04cMO=r^Xfw!=+_7YtGzh0Rj2jBXxg zuyX(C?zP7y(53!4`x-aQ1hq^ge)qxfa6DSi2PH06jod0bm7?s){!`9M**bRKxn+I- z`{KQK)m?+E&N2h*7p`*(+7oG(;?u zaF*EI8H@&#G&^t5!neOQ{+9Cb&K((0{S;_Z zEBVRud5UQgWyA4U2;n$(tyWXPl=j9TklkA!KJzSl*dc|*vCe=YAxxnT?NnwRV__bY z21H6o-j3eZH^!3-Rt_(CAIo$~)8(V|-IC?ZIm>CGYw5uwNMRQO$>%6 zC@zjC)if)_QXv2kvQ#kkCc72;!q&F@+>_6q%3nC)pP9=Q%4RduGleQ|m5z?Pjan4| z2qAjspi_*c0RX_Ggom2xUE6Z!t}Zv4wS%{A-fT4Loo*+XPK7Yf9UU~j_UAqV&EP9* z=dW(`YRzh4V`sTiy`S`Rf|jjXIt`MPa&RaiCyMSko|<%_S>c;*CKC$;WGD%WyjgB| zpV&+HN=D)S{jJe>s+0ymn{zs}n5PtE(l&XyYcDzj<=Z%V+?X=^h{u~0U8-F#tVx^m^jZqVwE4gg5eC@*>!3#MsuRmkLReDn@g&VywGB0RZ??zM!&bl4f@0Zo4~b zE-q<+rKJT#DEgES& zw?{dnIUha(4R-smBm}iBFbyT8ESCzN>$t8%EF%b_)^VFNp3mopS}(Rb{bD(GCwV!? zBN99^kxZA5tHGM^2?4D^DQTLfQi@)@Fj+elPzvTqICWD_(%gzai$;W&p1cOvG~aLO z&7Jh^59nf(EiI5r2>@mhH%vHg@s)W60Dz#iCX^5$fPhFSrHrv8NeY#+-rFx?iS$X4 zH6Bl{NHXU4{9GodRUw2@(lAV=6fnp#nPt-Ryyg0N%Mj4nA#zr8-L1hSZwp2#TV7BV zGw616sX4p#wxJnaT8ImCyW=>Q6HHV9fD7L34TWH-lGE{w3+C8%CNn~a);jV$b1 + + + + diff --git a/brouter-routing-app/res/values/strings.xml b/brouter-routing-app/res/values/strings.xml new file mode 100644 index 0000000..3f03430 --- /dev/null +++ b/brouter-routing-app/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + + BRouter + diff --git a/brouter-routing-app/src/main/java/btools/routingapp/BRouterActivity.java b/brouter-routing-app/src/main/java/btools/routingapp/BRouterActivity.java new file mode 100644 index 0000000..431dcf8 --- /dev/null +++ b/brouter-routing-app/src/main/java/btools/routingapp/BRouterActivity.java @@ -0,0 +1,375 @@ +package btools.routingapp; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorManager; +import android.os.Bundle; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.speech.tts.TextToSpeech.OnInitListener; +import android.view.Display; +import android.view.WindowManager; +import android.widget.EditText; +import btools.router.OsmNodeNamed; + +public class BRouterActivity extends Activity implements OnInitListener { + + private static final int DIALOG_SELECTPROFILE_ID = 1; + private static final int DIALOG_EXCEPTION_ID = 2; + private static final int DIALOG_WARNEXPIRY_ID = 3; + private static final int DIALOG_TEXTENTRY_ID = 4; + private static final int DIALOG_VIASELECT_ID = 5; + private static final int DIALOG_NOGOSELECT_ID = 6; + private static final int DIALOG_SHOWRESULT_ID = 7; + private static final int DIALOG_ROUTINGMODES_ID = 8; + private static final int DIALOG_MODECONFIGOVERVIEW_ID = 9; + private static final int DIALOG_PICKWAYPOINT_ID = 10; + + private BRouterView mBRouterView; + private PowerManager mPowerManager; + private WindowManager mWindowManager; + private Display mDisplay; + private WakeLock mWakeLock; + + /** Called when the activity is first created. */ + @Override + @SuppressWarnings("deprecation") + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Get an instance of the PowerManager + mPowerManager = (PowerManager) getSystemService(POWER_SERVICE); + + // Get an instance of the WindowManager + mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + mDisplay = mWindowManager.getDefaultDisplay(); + + // Create a bright wake lock + mWakeLock = mPowerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, getClass() + .getName()); + + // instantiate our simulation view and set it as the activity's content + mBRouterView = new BRouterView(this); + setContentView(mBRouterView); + } + + @Override + @SuppressWarnings("deprecation") + protected Dialog onCreateDialog(int id) + { + AlertDialog.Builder builder; + switch(id) + { + case DIALOG_SELECTPROFILE_ID: + builder = new AlertDialog.Builder(this); + builder.setTitle("Select a routing profile"); + builder.setItems(availableProfiles, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int item) { + selectedProfile = availableProfiles[item]; + mBRouterView.startProcessing(selectedProfile); + } + }); + return builder.create(); + case DIALOG_ROUTINGMODES_ID: + builder = new AlertDialog.Builder(this); + builder.setTitle( message ); + builder.setMultiChoiceItems(routingModes, routingModesChecked, new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, + boolean isChecked) { + routingModesChecked[which] = isChecked; + } + }); + builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + mBRouterView.configureService(routingModes,routingModesChecked); + } + }); + return builder.create(); + case DIALOG_EXCEPTION_ID: + builder = new AlertDialog.Builder(this); + builder.setTitle( "An Error occured" ) + .setMessage( errorMessage ) + .setPositiveButton( "OK", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + mBRouterView.continueProcessing(); + } + }); + return builder.create(); + case DIALOG_WARNEXPIRY_ID: + builder = new AlertDialog.Builder(this); + builder.setMessage( errorMessage ) + .setPositiveButton( "OK", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + mBRouterView.startProcessing(selectedProfile); + } + }); + return builder.create(); + case DIALOG_TEXTENTRY_ID: + builder = new AlertDialog.Builder(this); + builder.setTitle("Enter SDCARD base dir:"); + builder.setMessage(message); + final EditText input = new EditText(this); + input.setText( defaultbasedir ); + builder.setView(input); + builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + String basedir = input.getText().toString(); + mBRouterView.startSetup(basedir, true ); + } + }); + return builder.create(); + case DIALOG_VIASELECT_ID: + builder = new AlertDialog.Builder(this); + builder.setTitle("Check VIA Selection:"); + builder.setMultiChoiceItems(availableVias, getCheckedBooleanArray( availableVias.length ), + new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, + boolean isChecked) { + if (isChecked) + { + selectedVias.add(availableVias[which]); + } + else + { + selectedVias.remove(availableVias[which]); + } + } + }); + builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + mBRouterView.updateViaList( selectedVias ); + mBRouterView.startProcessing(selectedProfile); + } + }); + return builder.create(); + case DIALOG_NOGOSELECT_ID: + builder = new AlertDialog.Builder(this); + builder.setTitle("Check NoGo Selection:"); + String[] nogoNames = new String[nogoList.size()]; + for( int i=0; i 0 ? "Select to/via" : "Select from" ); + builder.setItems(availableWaypoints, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int item) { + mBRouterView.updateWaypointList( availableWaypoints[item] ); + mBRouterView.startProcessing(selectedProfile); + } + }); + return builder.create(); + + default: + return null; + } + + } + + private boolean[] getCheckedBooleanArray( int size ) + { + boolean[] checked = new boolean[size]; + for( int i=0; i selectedVias; + + private List nogoList; + + @SuppressWarnings("deprecation") + public void selectProfile( String[] items ) + { + availableProfiles = items; + showDialog( DIALOG_SELECTPROFILE_ID ); + } + + @SuppressWarnings("deprecation") + public void selectRoutingModes( String[] modes, boolean[] modesChecked, String message ) + { + routingModes = modes; + routingModesChecked = modesChecked; + this.message = message; + showDialog( DIALOG_ROUTINGMODES_ID ); + } + + @SuppressWarnings("deprecation") + public void showModeConfigOverview( String message ) + { + this.message = message; + showDialog( DIALOG_MODECONFIGOVERVIEW_ID ); + } + + + @SuppressWarnings("deprecation") + public void selectBasedir( String defaultBasedir, String message ) + { + this.defaultbasedir = defaultBasedir; + this.message = message; + showDialog( DIALOG_TEXTENTRY_ID ); + } + + @SuppressWarnings("deprecation") + public void selectVias( String[] items ) + { + availableVias = items; + selectedVias = new HashSet(availableVias.length); + for( String via : items ) selectedVias.add( via ); + showDialog( DIALOG_VIASELECT_ID ); + } + + @SuppressWarnings("deprecation") + public void selectWaypoint( String[] items ) + { + availableWaypoints = items; + showNewDialog( DIALOG_PICKWAYPOINT_ID ); + } + + @SuppressWarnings("deprecation") + public void selectNogos( List nogoList ) + { + this.nogoList = nogoList; + showDialog( DIALOG_NOGOSELECT_ID ); + } + + private Set dialogIds = new HashSet(); + + private void showNewDialog( int id ) + { + if ( dialogIds.contains( new Integer( id ) ) ) + { + removeDialog( id ); + } + dialogIds.add( new Integer( id ) ); + showDialog( id ); + } + + private String errorMessage; + private String title; + private int wpCount; + + @SuppressWarnings("deprecation") + public void showErrorMessage( String msg ) + { + errorMessage = msg; + showNewDialog( DIALOG_EXCEPTION_ID ); + } + + @SuppressWarnings("deprecation") + public void showResultMessage( String title, String msg, int wpCount ) + { + errorMessage = msg; + this.title = title; + this.wpCount = wpCount; + showNewDialog( DIALOG_SHOWRESULT_ID ); + } + + @Override + protected void onResume() { + super.onResume(); + /* + * when the activity is resumed, we acquire a wake-lock so that the + * screen stays on, since the user will likely not be fiddling with the + * screen or buttons. + */ + mWakeLock.acquire(); + + // Start the simulation + mBRouterView.startSimulation(); + + + } + + @Override + protected void onPause() { + super.onPause(); + /* + * When the activity is paused, we make sure to stop the simulation, + * release our sensor resources and wake locks + */ + + // Stop the simulation + mBRouterView.stopSimulation(); + + // and release our wake-lock + mWakeLock.release(); + + + } + + @Override + public void onInit(int i) + { + } + +} diff --git a/brouter-routing-app/src/main/java/btools/routingapp/BRouterService.java b/brouter-routing-app/src/main/java/btools/routingapp/BRouterService.java new file mode 100644 index 0000000..230170a --- /dev/null +++ b/brouter-routing-app/src/main/java/btools/routingapp/BRouterService.java @@ -0,0 +1,146 @@ +package btools.routingapp; + + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.StringTokenizer; + +import android.app.Service; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import btools.router.OsmNodeNamed; + +public class BRouterService extends Service +{ + + @Override + public IBinder onBind(Intent arg0) { + Log.d(getClass().getSimpleName(), "onBind()"); + return myBRouterServiceStub; + } + + private IBRouterService.Stub myBRouterServiceStub = new IBRouterService.Stub() + { + @Override + public String getTrackFromParams(Bundle params) throws RemoteException + { + BRouterWorker worker = new BRouterWorker(); + + // get base dir from private file + String baseDir = null; + InputStream configInput = null; + try + { + configInput = openFileInput( "config.dat" ); + BufferedReader br = new BufferedReader( new InputStreamReader (configInput ) ); + baseDir = br.readLine(); + } + catch( Exception e ) {} + finally + { + if ( configInput != null ) try { configInput.close(); } catch( Exception ee ) {} + } + + String fast = params.getString( "fast" ); + boolean isFast = "1".equals( fast ) || "true".equals( fast ) || "yes".equals( fast ); + String mode_key = params.getString( "v" ) + "_" + (isFast ? "fast" : "short"); + + boolean configFound = false; + + BufferedReader br = null; + try + { + String modesFile = baseDir + "/brouter/modes/serviceconfig.dat"; + br = new BufferedReader( new FileReader (modesFile ) ); + worker.segmentDir = baseDir + "/brouter/segments2"; + for(;;) + { + String line = br.readLine(); + if ( line == null ) break; + ServiceModeConfig smc = new ServiceModeConfig( line ); + if ( !smc.mode.equals( mode_key ) ) continue; + worker.profilePath = baseDir + "/brouter/profiles2/" + smc.profile + ".brf"; + worker.rawTrackPath = baseDir + "/brouter/modes/" + mode_key + "_rawtrack.dat"; + + CoordinateReader cor = CoordinateReader.obtainValidReader( baseDir ); + worker.nogoList = new ArrayList(); + // veto nogos by profiles veto list + for(OsmNodeNamed nogo : cor.nogopoints ) + { + if ( !smc.nogoVetos.contains( nogo.ilon + "," + nogo.ilat ) ) + { + worker.nogoList.add( nogo ); + } + } + configFound = true; + } + } + catch( Exception e ) + { + return "no brouter service config found, mode " + mode_key; + } + finally + { + if ( br != null ) try { br.close(); } catch( Exception ee ) {} + } + + if ( !configFound ) + { + return "no brouter service config found for mode " + mode_key; + } + + try + { + return worker.getTrackFromParams(params); + } + catch( IllegalArgumentException iae ) + { + return iae.getMessage(); + } + } + }; + + @Override + public void onCreate() + { + super.onCreate(); + Log.d(getClass().getSimpleName(),"onCreate()"); + } + + @Override + public void onDestroy() + { + super.onDestroy(); + Log.d(getClass().getSimpleName(),"onDestroy()"); + } + + + // This is the old onStart method that will be called on the pre-2.0 + // platform. On 2.0 or later we override onStartCommand() so this + // method will not be called. + @Override + @SuppressWarnings("deprecation") + public void onStart(Intent intent, int startId) + { + Log.d(getClass().getSimpleName(), "onStart()"); + handleStart(intent, startId); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) + { + handleStart(intent, startId); + return START_STICKY; + } + + void handleStart(Intent intent, int startId) + { + } +} diff --git a/brouter-routing-app/src/main/java/btools/routingapp/BRouterView.java b/brouter-routing-app/src/main/java/btools/routingapp/BRouterView.java new file mode 100644 index 0000000..4890792 --- /dev/null +++ b/brouter-routing-app/src/main/java/btools/routingapp/BRouterView.java @@ -0,0 +1,760 @@ +package btools.routingapp; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.os.Environment; +import android.util.DisplayMetrics; +import android.view.View; +import android.widget.Toast; +import btools.expressions.BExpressionContext; +import btools.mapaccess.OsmNode; +import btools.router.OsmNodeNamed; +import btools.router.OsmTrack; +import btools.router.RoutingContext; +import btools.router.RoutingEngine; + +public class BRouterView extends View +{ + RoutingEngine cr; + private int imgw; + private int imgh; + + private int centerLon; + private int centerLat; + private double scaleLon; + private double scaleLat; + private List wpList; + private List nogoList; + private List nogoVetoList; + private OsmTrack rawTrack; + + private String modesDir; + private String tracksDir; + private String segmentDir; + private String profileDir; + private String profilePath; + private String profileName; + private String sourceHint; + private boolean waitingForSelection = false; + + private boolean needsViaSelection; + private boolean needsNogoSelection; + private boolean needsWaypointSelection; + + private long lastDataTime = System.currentTimeMillis(); + + private CoordinateReader cor; + + private int[] imgPixels; + + public void startSimulation() { + } + + public void stopSimulation() { + if ( cr != null ) cr.terminate(); + } + + public BRouterView(Context context) { + super(context); + + DisplayMetrics metrics = new DisplayMetrics(); + ((Activity)getContext()).getWindowManager().getDefaultDisplay().getMetrics(metrics); + imgw = metrics.widthPixels; + imgh = metrics.heightPixels; + + // get base dir from private file + String baseDir = null; + InputStream configInput = null; + try + { + configInput = getContext().openFileInput( "config.dat" ); + BufferedReader br = new BufferedReader( new InputStreamReader (configInput ) ); + baseDir = br.readLine(); + } + catch( Exception e ) {} + finally + { + if ( configInput != null ) try { configInput.close(); } catch( Exception ee ) {} + } + // check if valid + boolean bdValid = false; + if ( baseDir != null ) + { + File bd = new File( baseDir ); + bdValid = bd.isDirectory(); + File brd = new File( bd, "brouter" ); + if ( brd.isDirectory() ) + { + startSetup( baseDir, false ); + return; + } + } + String message = baseDir == null ? + "(no basedir configured previously)" : + "(previous basedir " + baseDir + + ( bdValid ? " does not contain 'brouter' subfolder)" + : " is not valid)" ); + + ((BRouterActivity)getContext()).selectBasedir( guessBaseDir(), message ); + waitingForSelection = true; + } + + public void startSetup( String baseDir, boolean storeBasedir ) + { + File fbd = new File( baseDir ); + if ( !fbd.isDirectory() ) + { + throw new IllegalArgumentException( "Base-directory " + baseDir + " is not a directory " ); + } + String basedir = fbd.getAbsolutePath(); + + if ( storeBasedir ) + { + BufferedWriter bw = null; + try + { + OutputStream configOutput = getContext().openFileOutput( "config.dat", Context.MODE_PRIVATE ); + bw = new BufferedWriter( new OutputStreamWriter (configOutput ) ); + bw.write( baseDir ); + bw.write( '\n' ); + } + catch( Exception e ) {} + finally + { + if ( bw != null ) try { bw.close(); } catch( Exception ee ) {} + } + } + + cor = null; + try + { + // create missing directories + assertDirectoryExists( "project directory", basedir + "/brouter" ); + segmentDir = basedir + "/brouter/segments2"; + assertDirectoryExists( "map directory", segmentDir ); + profileDir = basedir + "/brouter/profiles2"; + assertDirectoryExists( "profile directory", profileDir ); + modesDir = basedir + "/brouter/modes"; + assertDirectoryExists( "modes directory", modesDir ); + + cor = CoordinateReader.obtainValidReader( basedir ); + wpList = cor.waypoints; + nogoList = cor.nogopoints; + nogoVetoList = new ArrayList(); + + sourceHint = "(coordinate-source: " + cor.basedir + cor.rootdir + ")"; + + needsViaSelection = wpList.size() > 2; + needsNogoSelection = nogoList.size() > 0; + needsWaypointSelection = wpList.size() == 0; + + if ( cor.tracksdir != null ) + { + tracksDir = cor.basedir + cor.tracksdir; + assertDirectoryExists( "track directory", tracksDir ); + + // output redirect: look for a pointerfile in tracksdir + File tracksDirPointer = new File( tracksDir + "/brouter.redirect" ); + if ( tracksDirPointer.isFile() ) + { + tracksDir = readSingleLineFile( tracksDirPointer ); + if ( tracksDir == null ) throw new IllegalArgumentException( "redirect pointer file is empty: " + tracksDirPointer ); + if ( !(new File( tracksDir ).isDirectory()) ) throw new IllegalArgumentException( + "redirect pointer file " + tracksDirPointer + " does not point to a directory: " + tracksDir ); + } + } + + boolean segmentFound = false; + String[] fileNames = new File( segmentDir ).list(); + for( String fileName : fileNames ) + { + if ( fileName.endsWith( ".rd5" ) ) segmentFound = true; + } + File carSubset = new File( segmentDir, "carsubset" ); + if ( carSubset.isDirectory() ) + { + fileNames = carSubset.list(); + for( String fileName : fileNames ) + { + if ( fileName.endsWith( ".cd5" ) ) segmentFound = true; + } + } + if ( !segmentFound ) + { + throw new IllegalArgumentException( "The segments-directory " + segmentDir + + " contains no routing data files (*.rd5)." + + " see www.dr-brenschede.de/brouter for setup instructions." ); + } + + fileNames = new File( profileDir ).list(); + ArrayList profiles = new ArrayList(); + + boolean lookupsFound = false; + for( String fileName : fileNames ) + { + if ( fileName.endsWith( ".brf" ) ) + { + profiles.add( fileName.substring( 0, fileName.length()-4 ) ); + } + if ( fileName.equals( "lookups.dat" ) ) lookupsFound = true; + } + if ( !lookupsFound ) + { + throw new IllegalArgumentException( "The profile-directory " + profileDir + + " does not contain the lookups.dat file." + + " see www.dr-brenschede.de/brouter for setup instructions." ); + } + if ( profiles.size() == 0 ) + { + throw new IllegalArgumentException( "The profile-directory " + profileDir + + " contains no routing profiles (*.brf)." + + " see www.dr-brenschede.de/brouter for setup instructions." ); + } + ((BRouterActivity)getContext()).selectProfile( profiles.toArray( new String[0]) ); + } + catch( Exception e ) + { + String msg = e instanceof IllegalArgumentException + ? e.getMessage() + ( cor == null ? "" : " (coordinate-source: " + cor.basedir + cor.rootdir + ")" ) + : e.toString(); + ((BRouterActivity)getContext()).showErrorMessage( msg ); + } + waitingForSelection = true; + } + + public void continueProcessing() + { + waitingForSelection = false; + invalidate(); + } + + public void updateViaList( Set selectedVias ) + { + ArrayList filtered = new ArrayList(wpList.size()); + for( OsmNodeNamed n : wpList ) + { + String name = n.name; + if ( "from".equals( name ) || "to".equals(name) || selectedVias.contains( name ) ) + filtered.add( n ); + } + wpList = filtered; + } + + public void updateNogoList( boolean[] enabled ) + { + for( int i=nogoList.size()-1; i >= 0; i-- ) + { + if ( !enabled[i] ) + { + nogoVetoList.add( nogoList.get(i) ); + nogoList.remove( i ); + } + } + } + + public void pickWaypoints() + { + String msg = null; + + Map allpoints = cor.allpoints; + if ( allpoints == null ) + { + allpoints = new TreeMap(); + cor.allpoints = allpoints; + try { cor.readFromTo(); } catch ( Exception e ) { msg = "Error reading waypoints: " + e.toString(); } + if ( allpoints.size() < 2 ) msg = "coordinate source does not contain enough waypoints: " + allpoints.size(); + if ( allpoints.size() > 100 ) msg = "coordinate source contains too much waypoints: " + allpoints.size() + "(please use from/to/via names)"; + } + if ( allpoints.size() < 1 ) msg = "no more wayoints available!"; + + if ( msg != null ) + { + ((BRouterActivity)getContext()).showErrorMessage( msg ); + } + else + { + String[] wpts = new String[allpoints.size()]; + int i = 0; + for( OsmNodeNamed wp : allpoints.values() ) wpts[i++] = wp.name; +System.out.println( "calling selectWaypoint..." ); + ((BRouterActivity)getContext()).selectWaypoint( wpts ); + } + } + + public void updateWaypointList( String waypoint ) + { + wpList.add( cor.allpoints.get( waypoint ) ); + cor.allpoints.remove( waypoint ); +System.out.println( "updateWaypointList: " + waypoint + " wpList.size()=" + wpList.size() ); + } + + public void finishWaypointSelection() + { + needsWaypointSelection = false; + } + + public void startProcessing( String profile ) + { + profilePath = profileDir + "/" + profile + ".brf"; + profileName = profile; + + if ( needsViaSelection ) + { + needsViaSelection = false; + String[] availableVias = new String[wpList.size()-2]; + for( int viaidx=0; viaidx0?"->" : "") + wpList.get(i).name; + } + ((BRouterActivity)getContext()).showResultMessage( "Select Action", msg, wpList.size() ); + return; + } + + try + { + waitingForSelection = false; + + RoutingContext rc = new RoutingContext(); + + // TODO: TEST! + // rc.rawTrackPath = "/mnt/sdcard/brouter/modes/bicycle_fast_rawtrack.dat"; + + rc.localFunction = profilePath; + + int plain_distance = 0; + int maxlon = Integer.MIN_VALUE; + int minlon = Integer.MAX_VALUE; + int maxlat = Integer.MIN_VALUE; + int minlat = Integer.MAX_VALUE; + + OsmNode prev = null; + for( OsmNode n : wpList ) + { + maxlon = n.ilon > maxlon ? n.ilon : maxlon; + minlon = n.ilon < minlon ? n.ilon : minlon; + maxlat = n.ilat > maxlat ? n.ilat : maxlat; + minlat = n.ilat < minlat ? n.ilat : minlat; + if ( prev != null ) + { + plain_distance += n.calcDistance( prev ); + } + prev = n; + } + toast( "Plain distance = " + plain_distance/1000. + " km" ); + + centerLon = (maxlon + minlon)/2; + centerLat = (maxlat + minlat)/2; + + double coslat = Math.cos( ((centerLat / 1000000.) - 90.) / 57.3 ) ; + double difflon = maxlon - minlon; + double difflat = maxlat - minlat; + + scaleLon = imgw / (difflon*1.5); + scaleLat = imgh / (difflat*1.5); + if ( scaleLon < scaleLat*coslat ) scaleLat = scaleLon/coslat; + else scaleLon = scaleLat*coslat; + + startTime = System.currentTimeMillis(); + rc.prepareNogoPoints( nogoList ); + rc.nogopoints = nogoList; + + cr = new RoutingEngine( tracksDir + "/brouter", null, segmentDir, wpList, rc ); + cr.start(); + invalidate(); + + } + catch( Exception e ) + { + String msg = e instanceof IllegalArgumentException ? e.getMessage() : e.toString(); + toast( msg ); + } + } + + private void assertDirectoryExists( String message, String path ) + { + File f = new File( path ); + f.mkdirs(); + if ( !f.exists() || !f.isDirectory() ) throw new IllegalArgumentException( message + ": " + path + " cannot be created" ); + } + + private void paintPosition( int ilon, int ilat, int color, int with ) + { + int lon = ilon - centerLon; + int lat = ilat - centerLat; + int x = imgw/2 + (int)(scaleLon*lon); + int y = imgh/2 - (int)(scaleLat*lat); + for( int nx=x-with; nx<=x+with; nx++) + for( int ny=y-with; ny<=y+with; ny++) + { + if ( nx >= 0 && nx < imgw && ny >= 0 && ny < imgh ) + { + imgPixels[ nx+imgw*ny] = color; + } + } + } + + private void paintCircle( Canvas canvas, OsmNodeNamed n, int color, int minradius ) + { + int lon = n.ilon - centerLon; + int lat = n.ilat - centerLat; + int x = imgw/2 + (int)(scaleLon*lon); + int y = imgh/2 - (int)(scaleLat*lat); + int ir = (int)(n.radius * 1000000. * scaleLat); + if ( ir > minradius ) + { + Paint paint = new Paint(); + paint.setColor( Color.RED ); + paint.setStyle( Paint.Style.STROKE ); + canvas.drawCircle( (float)x, (float)y, (float)ir, paint ); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + } + + private void toast( String msg ) + { + Toast.makeText(getContext(), msg, Toast.LENGTH_LONG ).show(); + lastDataTime += 4000; // give time for the toast before exiting + } + + +private long lastTs = System.currentTimeMillis(); +private long startTime = 0L; + + @Override + protected void onDraw(Canvas canvas) { + try + { + _onDraw( canvas ); + } + catch( Throwable t ) + { + // on out of mem, try to stop the show + String hint = ""; + if ( cr != null ) hint = cr.cleanOnOOM(); + cr = null; + try { Thread.sleep( 2000 ); } catch( InterruptedException ie ) {} + ((BRouterActivity)getContext()).showErrorMessage( t.toString() + hint ); + waitingForSelection = true; + } + } + + private void _onDraw(Canvas canvas) { + + if ( waitingForSelection ) return; + + long currentTs = System.currentTimeMillis(); + long diffTs = currentTs - lastTs; + long sleeptime = 500 - diffTs; + while ( sleeptime < 200 ) sleeptime += 500; + + try { Thread.sleep( sleeptime ); } catch ( InterruptedException ie ) {} + lastTs = System.currentTimeMillis(); + + if ( cr == null || cr.isFinished() ) + { + if ( cr != null ) + { + if ( cr.getErrorMessage() != null ) + { + ((BRouterActivity)getContext()).showErrorMessage( cr.getErrorMessage() ); + cr = null; + waitingForSelection = true; + return; + } + else + { + String result = "version = BRouter-0.98\n" + + "distance = " + cr.getDistance()/1000. + " km\n" + + "filtered ascend = " + cr.getAscend() + " m\n" + + "plain ascend = " + cr.getPlainAscend(); + + rawTrack = cr.getFoundRawTrack(); + + String title = "Success"; + if ( cr.getAlternativeIndex() > 0 ) title += " / " + cr.getAlternativeIndex() + ". Alternative"; + + ((BRouterActivity)getContext()).showResultMessage( title, result, -1 ); + cr = null; + waitingForSelection = true; + return; + } + } + else if ( System.currentTimeMillis() > lastDataTime ) + { + System.exit(0); + } + } + else + { + lastDataTime = System.currentTimeMillis(); + imgPixels = new int[imgw*imgh]; + + int[] openSet = cr.getOpenSet(); + for( int si = 0; si < openSet.length; si += 2 ) + { + paintPosition( openSet[si], openSet[si+1], 0xffffff, 1 ); + } + // paint nogos on top (red) + for( int ngi=0; ngi basedirGuesses = new ArrayList(); + basedirGuesses.add( basedir.getAbsolutePath() ); + + if ( bd2.exists() ) + { + basedir = bd2; + basedirGuesses.add( basedir.getAbsolutePath() ); + } + + ArrayList rl = new ArrayList(); + for( String bdg : basedirGuesses ) + { + rl.add( new CoordinateReaderOsmAnd(bdg) ); + rl.add( new CoordinateReaderLocus(bdg) ); + rl.add( new CoordinateReaderOrux(bdg) ); + } + long tmax = 0; + CoordinateReader cor = null; + for( CoordinateReader r : rl ) + { + long t = r.getTimeStamp(); + if ( t > tmax ) + { + tmax = t; + cor = r; + } + } + if ( cor != null ) + { + return cor.basedir; + } + } + catch( Exception e ) + { + System.out.println( "guessBaseDir:" + e ); + } + return basedir.getAbsolutePath(); + } + + public void writeRawTrackToMode( String mode ) + { + // plus eventually the raw track for re-use + String rawTrackPath = modesDir + "/" + mode + "_rawtrack.dat"; + if ( rawTrack != null ) + { + try + { + rawTrack.writeBinary( rawTrackPath ); + } + catch( Exception e ) {} + } + else + { + new File( rawTrackPath ).delete(); + } + } + + public void startConfigureService() + { + String[] modes = new String[] { + "foot_short", "foot_fast", + "bicycle_short", "bicycle_fast", + "motorcar_short", "motorcar_fast" + }; + boolean[] modesChecked = new boolean[6]; + + // parse global section of profile for mode preselection + BExpressionContext expctxGlobal = new BExpressionContext( "global" ); + expctxGlobal.readMetaData( new File( profileDir, "lookups.dat" ) ); + expctxGlobal.parseFile( new File( profilePath ), null ); + expctxGlobal.evaluate( 1L, null ); + boolean isFoot = 0.f != expctxGlobal.getVariableValue( "validForFoot" ); + boolean isBike = 0.f != expctxGlobal.getVariableValue( "validForBikes" ); + boolean isCar = 0.f != expctxGlobal.getVariableValue( "validForCars" ); + + if ( isFoot || isBike || isCar ) + { + modesChecked[ 0 ] = isFoot; + modesChecked[ 1 ] = isFoot; + modesChecked[ 2 ] = isBike; + modesChecked[ 3 ] = isBike; + modesChecked[ 4 ] = isCar; + modesChecked[ 5 ] = isCar; + } + else + { + for( int i=0; i<6; i++) + { + modesChecked[i] = true; + } + } + String msg = "Choose service-modes to configure (" + profileName + " [" + nogoVetoList.size() + "])"; + + ((BRouterActivity)getContext()).selectRoutingModes( modes, modesChecked, msg ); + } + + public void configureService(String[] routingModes, boolean[] checkedModes) + { + // read in current config + TreeMap map = new TreeMap(); + BufferedReader br = null; + String modesFile = modesDir + "/serviceconfig.dat"; + try + { + br = new BufferedReader( new FileReader (modesFile ) ); + for(;;) + { + String line = br.readLine(); + if ( line == null ) break; + ServiceModeConfig smc = new ServiceModeConfig( line ); + map.put( smc.mode, smc ); + } + } + catch( Exception e ) {} + finally + { + if ( br != null ) try { br.close(); } catch( Exception ee ) {} + } + + // replace selected modes + for( int i=0; i<6; i++) + { + if ( checkedModes[i] ) + { + writeRawTrackToMode( routingModes[i] ); + ServiceModeConfig smc = new ServiceModeConfig( routingModes[i], profileName); + for( OsmNodeNamed nogo : nogoVetoList) + { + smc.nogoVetos.add( nogo.ilon + "," + nogo.ilat ); + } + map.put( smc.mode, smc ); + } + } + + + // no write new config + BufferedWriter bw = null; + StringBuilder msg = new StringBuilder( "Mode mapping is now:\n" ); + msg.append( "( [..] counts nogo-vetos)\n" ); + try + { + bw = new BufferedWriter( new FileWriter ( modesFile ) ); + for( ServiceModeConfig smc : map.values() ) + { + bw.write( smc.toLine() ); + bw.write( '\n' ); + msg.append( smc.toString() ).append( '\n' ); + } + } + catch( Exception e ) {} + finally + { + if ( bw != null ) try { bw.close(); } catch( Exception ee ) {} + } + ((BRouterActivity)getContext()).showModeConfigOverview( msg.toString() ); + } + + private String readSingleLineFile( File f ) + { + BufferedReader br = null; + try + { + br = new BufferedReader( new InputStreamReader ( new FileInputStream( f ) ) ); + return br.readLine(); + } + catch( Exception e ) { return null; } + finally + { + if ( br != null ) try { br.close(); } catch( Exception ee ) {} + } + } + +} diff --git a/brouter-routing-app/src/main/java/btools/routingapp/BRouterWorker.java b/brouter-routing-app/src/main/java/btools/routingapp/BRouterWorker.java new file mode 100644 index 0000000..7f8f11b --- /dev/null +++ b/brouter-routing-app/src/main/java/btools/routingapp/BRouterWorker.java @@ -0,0 +1,139 @@ +package btools.routingapp; + + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import android.os.Bundle; +import btools.router.RoutingEngine; +import btools.router.OsmNodeNamed; +import btools.router.OsmTrack; +import btools.router.RoutingContext; + +public class BRouterWorker +{ + public String segmentDir; + public String profilePath; + public String rawTrackPath; + public List nogoList; + + public String getTrackFromParams(Bundle params) + { + String pathToFileResult = params.getString("pathToFileResult"); + + if (pathToFileResult != null) + { + File f = new File (pathToFileResult); + File dir = f.getParentFile(); + if (!dir.exists() || !dir.canWrite()){ + return "file folder does not exists or can not be written!"; + } + } + + long maxRunningTime = 60000; + String sMaxRunningTime = params.getString( "maxRunningTime" ); + if ( sMaxRunningTime != null ) + { + maxRunningTime = Integer.parseInt( sMaxRunningTime ) * 1000; + } + + RoutingContext rc = new RoutingContext(); + rc.rawTrackPath = rawTrackPath; + rc.localFunction = profilePath; + if ( nogoList != null ) + { + rc.prepareNogoPoints( nogoList ); + rc.nogopoints = nogoList; + } + + readNogos( params ); // add interface provides nogos + + RoutingEngine cr = new RoutingEngine( null, null, segmentDir, readPositions(params), rc ); + cr.quite = true; + cr.doRun( maxRunningTime ); + if ( cr.getErrorMessage() != null ) + { + return cr.getErrorMessage(); + } + + // store new reference track if any + if ( cr.getFoundRawTrack() != null ) + { + try + { + cr.getFoundRawTrack().writeBinary( rawTrackPath ); + } + catch( Exception e ) {} + } + + + String format = params.getString("trackFormat"); + boolean writeKml = format != null && "kml".equals( format ); + + OsmTrack track = cr.getFoundTrack(); + if ( track != null ) + { + if ( pathToFileResult == null ) + { + if ( writeKml ) return track.formatAsKml(); + return track.formatAsGpx(); + } + try + { + if ( writeKml ) track.writeKml(pathToFileResult); + else track.writeGpx(pathToFileResult); + } + catch( Exception e ) + { + return "error writing file: " + e; + } + } + return null; + } + + private List readPositions( Bundle params ) + { + List wplist = new ArrayList(); + + double[] lats = params.getDoubleArray("lats"); + double[] lons = params.getDoubleArray("lons"); + + if (lats == null || lats.length < 2 || lons == null || lons.length < 2) + { + throw new IllegalArgumentException( "we need two lat/lon points at least!" ); + } + + for( int i=0; i waypoints; + public List nogopoints; + public String basedir; + public String rootdir; + public String tracksdir; + + public Map allpoints; + private HashMap pointmap; + + protected static String[] posnames + = new String[]{ "from", "via1", "via2", "via3", "via4", "via5", "via6", "via7", "via8", "via9", "to" }; + + public CoordinateReader( String basedir ) + { + this.basedir = basedir; + } + + public abstract long getTimeStamp() throws Exception; + + /* + * read the from, to and via-positions from a gpx-file + */ + public void readFromTo() throws Exception + { + pointmap = new HashMap(); + waypoints = new ArrayList(); + nogopoints = new ArrayList(); + readPointmap(); + boolean fromToMissing = false; + for( int i=0; i rl = new ArrayList(); + rl.add( new CoordinateReaderOsmAnd(basedir) ); + rl.add( new CoordinateReaderLocus(basedir) ); + rl.add( new CoordinateReaderOrux(basedir) ); + + // eventually add standard-sd + File standardbase = Environment.getExternalStorageDirectory(); + if ( standardbase != null ) + { + String base2 = standardbase.getAbsolutePath(); + if ( !base2.equals( basedir ) ) + { + rl.add( new CoordinateReaderOsmAnd(base2) ); + rl.add( new CoordinateReaderLocus(base2) ); + rl.add( new CoordinateReaderOrux(base2) ); + } + } + + long tmax = 0; + for( CoordinateReader r : rl ) + { + long t = r.getTimeStamp(); + if ( t > tmax ) + { + tmax = t; + cor = r; + } + } + if ( cor == null ) + { + cor = new CoordinateReaderNone(); + } + cor.readFromTo(); + return cor; + } +} diff --git a/brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderLocus.java b/brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderLocus.java new file mode 100644 index 0000000..21de52c --- /dev/null +++ b/brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderLocus.java @@ -0,0 +1,53 @@ +package btools.routingapp; + +import java.io.File; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; +import btools.router.OsmNodeNamed; + +/** + * Read coordinates from a gpx-file + */ +public class CoordinateReaderLocus extends CoordinateReader +{ + public CoordinateReaderLocus( String basedir ) + { + super( basedir ); + tracksdir = "/Locus/mapItems"; + rootdir = "/Locus"; + } + + @Override + public long getTimeStamp() throws Exception + { + long t1 = new File( basedir + "/Locus/data/database/waypoints.db" ).lastModified(); + return t1; + } + + /* + * read the from and to position from a ggx-file + * (with hardcoded name for now) + */ + @Override + public void readPointmap() throws Exception + { + _readPointmap( basedir + "/Locus/data/database/waypoints.db" ); + } + + private void _readPointmap( String filename ) throws Exception + { + SQLiteDatabase myDataBase = SQLiteDatabase.openDatabase( filename, null, SQLiteDatabase.OPEN_READONLY); + Cursor c = myDataBase.rawQuery("SELECT name, longitude, latitude FROM waypoints", null); + while (c.moveToNext()) + { + OsmNodeNamed n = new OsmNodeNamed(); + n.name = c.getString(0); + n.ilon = (int)( ( Double.parseDouble( c.getString(1) ) + 180. )*1000000. + 0.5); + n.ilat = (int)( ( Double.parseDouble( c.getString(2) ) + 90. )*1000000. + 0.5); + checkAddPoint( n ); + } + myDataBase.close(); + } +} diff --git a/brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderNone.java b/brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderNone.java new file mode 100644 index 0000000..a92f7e6 --- /dev/null +++ b/brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderNone.java @@ -0,0 +1,26 @@ +package btools.routingapp; + + +/** + * Dummy coordinate reader if none found + */ +public class CoordinateReaderNone extends CoordinateReader +{ + public CoordinateReaderNone() + { + super( "" ); + rootdir = "none"; + } + + @Override + public long getTimeStamp() throws Exception + { + return 0L; + } + + @Override + public void readPointmap() throws Exception + { + } + +} diff --git a/brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderOrux.java b/brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderOrux.java new file mode 100644 index 0000000..36f1af6 --- /dev/null +++ b/brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderOrux.java @@ -0,0 +1,52 @@ +package btools.routingapp; + +import java.io.File; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import btools.router.OsmNodeNamed; + +/** + * Read coordinates from a gpx-file + */ +public class CoordinateReaderOrux extends CoordinateReader +{ + public CoordinateReaderOrux( String basedir ) + { + super( basedir ); + tracksdir = "/oruxmaps/tracklogs"; + rootdir = "/oruxmaps"; + } + + @Override + public long getTimeStamp() throws Exception + { + long t1 = new File( basedir + "/oruxmaps/tracklogs/oruxmapstracks.db" ).lastModified(); + return t1; + } + + /* + * read the from and to position from a ggx-file + * (with hardcoded name for now) + */ + @Override + public void readPointmap() throws Exception + { + _readPointmap( basedir + "/oruxmaps/tracklogs/oruxmapstracks.db" ); + } + + private void _readPointmap( String filename ) throws Exception + { + SQLiteDatabase myDataBase = SQLiteDatabase.openDatabase( filename, null, SQLiteDatabase.OPEN_READONLY); + Cursor c = myDataBase.rawQuery("SELECT poiname, poilon, poilat FROM pois", null); + while (c.moveToNext()) + { + OsmNodeNamed n = new OsmNodeNamed(); + n.name = c.getString(0); + n.ilon = (int)( ( Double.parseDouble( c.getString(1) ) + 180. )*1000000. + 0.5); + n.ilat = (int)( ( Double.parseDouble( c.getString(2) ) + 90. )*1000000. + 0.5); + checkAddPoint( n ); + } + myDataBase.close(); + } +} diff --git a/brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderOsmAnd.java b/brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderOsmAnd.java new file mode 100644 index 0000000..bf8a22f --- /dev/null +++ b/brouter-routing-app/src/main/java/btools/routingapp/CoordinateReaderOsmAnd.java @@ -0,0 +1,87 @@ +package btools.routingapp; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; + +import btools.router.OsmNodeNamed; + +/** + * Read coordinates from a gpx-file + */ +public class CoordinateReaderOsmAnd extends CoordinateReader +{ + public CoordinateReaderOsmAnd( String basedir ) + { + super( basedir ); + tracksdir = "/osmand/tracks"; + rootdir = "/osmand"; + } + + @Override + public long getTimeStamp() throws Exception + { + long t1 = new File( basedir + "/osmand/favourites_bak.gpx" ).lastModified(); + long t2 = new File( basedir + "/osmand/favourites.gpx" ).lastModified(); + return t1 > t2 ? t1 : t2; + } + + /* + * read the from and to position from a gpx-file + * (with hardcoded name for now) + */ + @Override + public void readPointmap() throws Exception + { + try + { + _readPointmap( basedir + "/osmand/favourites_bak.gpx" ); + } + catch( Exception e ) + { + _readPointmap( basedir + "/osmand/favourites.gpx" ); + } + } + + private void _readPointmap( String filename ) throws Exception + { + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream( filename ) ) ); + OsmNodeNamed n = null; + + for(;;) + { + String line = br.readLine(); + if ( line == null ) break; + + int idx0 = line.indexOf( "" ); + if ( idx0 >= 0 ) + { + n = new OsmNodeNamed(); + idx0 += 10; + int idx1 = line.indexOf( '"', idx0 ); + n.ilat = (int)( (Double.parseDouble( line.substring( idx0, idx1 ) ) + 90. )*1000000. + 0.5); + int idx2 = line.indexOf( " lon=\"" ); + if ( idx2 < 0 ) continue; + idx2 += 6; + int idx3 = line.indexOf( '"', idx2 ); + n.ilon = (int)( ( Double.parseDouble( line.substring( idx2, idx3 ) ) + 180. )*1000000. + 0.5); + continue; + } + if ( n != null && idx10 >= 0 ) + { + idx10 += 6; + int idx11 = line.indexOf( "", idx10 ); + if ( idx11 >= 0 ) + { + n.name = line.substring( idx10, idx11 ).trim(); + checkAddPoint( n ); + } + } + } + br.close(); + } +} diff --git a/brouter-routing-app/src/main/java/btools/routingapp/IBRouterService.aidl b/brouter-routing-app/src/main/java/btools/routingapp/IBRouterService.aidl new file mode 100644 index 0000000..55b225f --- /dev/null +++ b/brouter-routing-app/src/main/java/btools/routingapp/IBRouterService.aidl @@ -0,0 +1,23 @@ +package btools.routingapp; + + +interface IBRouterService { + + + //param params--> Map of params: + // "pathToFileResult"-->String with the path to where the result must be saved, including file name and extension + // -->if null, the track is passed via the return argument + // "maxRunningTime"-->String with a number of seconds for the routing timeout, default = 60 + // "trackFormat"-->[kml|gpx] default = gpx + // "lats"-->double[] array of latitudes; 2 values at least. + // "lons"-->double[] array of longitudes; 2 values at least. + // "nogoLats"-->double[] array of nogo latitudes; may be null. + // "nogoLons"-->double[] array of nogo longitudes; may be null. + // "nogoRadi"-->double[] array of nogo radius in meters; may be null. + // "fast"-->[0|1] + // "v"-->[motorcar|bicycle|foot] + //return null if all ok and no path given, the track if ok and path given, an error message if it was wrong + //call in a background thread, heavy task! + + String getTrackFromParams(in Bundle params); +} diff --git a/brouter-routing-app/src/main/java/btools/routingapp/ServiceModeConfig.java b/brouter-routing-app/src/main/java/btools/routingapp/ServiceModeConfig.java new file mode 100644 index 0000000..99c36ad --- /dev/null +++ b/brouter-routing-app/src/main/java/btools/routingapp/ServiceModeConfig.java @@ -0,0 +1,50 @@ +package btools.routingapp; + +import java.util.StringTokenizer; +import java.util.TreeSet; + + +/** + * Decsription of a service config + */ +public class ServiceModeConfig +{ + public String mode; + public String profile; + public TreeSet nogoVetos; + + public ServiceModeConfig( String line ) + { + StringTokenizer tk = new StringTokenizer( line ); + mode = tk.nextToken(); + profile = tk.nextToken(); + nogoVetos = new TreeSet(); + while( tk.hasMoreTokens() ) + { + nogoVetos.add( tk.nextToken() ); + } + } + + public ServiceModeConfig( String mode, String profile ) + { + this.mode = mode; + this.profile = profile; + nogoVetos = new TreeSet(); + } + + public String toLine() + { + StringBuilder sb = new StringBuilder( 100 ); + sb.append( mode ).append( ' ' ).append( profile ); + for( String veto: nogoVetos ) sb.append( ' ' ).append( veto ); + return sb.toString(); + } + + public String toString() + { + StringBuilder sb = new StringBuilder( 100 ); + sb.append( mode ).append( "->" ).append( profile ); + sb.append ( " [" + nogoVetos.size() + "]" ); + return sb.toString(); + } +} diff --git a/brouter-routing-app/src/main/resources/brouter/profiles2/car-test.brf b/brouter-routing-app/src/main/resources/brouter/profiles2/car-test.brf new file mode 100644 index 0000000..b2c2ce7 --- /dev/null +++ b/brouter-routing-app/src/main/resources/brouter/profiles2/car-test.brf @@ -0,0 +1,105 @@ +# +# Car-Routing is experimantal !!! +# +# DO NOT USE FOR ACTUAL NAVIGATION +# +# Turn restrictions are missing, leading to wrong routes +# + +---context:global + +assign downhillcost 0 +assign downhillcutoff 0 +assign uphillcost 0 +assign uphillcutoff 0 + +---context:way # following code refers to way-tags + +assign turncost 200 +assign initialcost switch highway=ferry 20000 0 + + +# +# calculate logical car access +# +assign caraccess + switch motorcar= + switch motor_vehicle= + switch vehicle= + switch access= + switch or highway=motorway highway=motorway_link 1 + switch or highway=trunk highway=trunk_link 1 + switch or highway=primary highway=primary_link 1 + switch or highway=secondary highway=secondary_link 1 + switch or highway=tertiary highway=tertiary_link 1 + switch highway=unclassified 1 + switch highway=ferry 1 + switch or highway=residential highway=living_street 1 + switch highway=service 1 + 0 + or access=yes or access=designated access=destination + or vehicle=yes or vehicle=designated vehicle=destination + or motor_vehicle=yes or motor_vehicle=designated motor_vehicle=destination + or motorcar=yes or motorcar=designated motorcar=destination + +assign accesspenalty + switch caraccess + 0 + 10000 + +assign onewaypenalty + switch switch reversedirection=yes + switch oneway= + junction=roundabout + or oneway=yes or oneway=true oneway=1 + oneway=-1 + 10000 + 0.0 + + +assign ispaved or surface=paved or surface=asphalt or surface=concrete surface=paving_stones + +assign costfactor + + add max onewaypenalty accesspenalty + + switch or highway=motorway highway=motorway_link 1 + switch or highway=trunk highway=trunk_link 1 + switch or highway=primary highway=primary_link switch maxspeed=30 2.0 switch maxspeed=50 1.5 1.2 + switch or highway=secondary highway=secondary_link 1.3 + switch or highway=tertiary highway=tertiary_link 1.4 + switch highway=unclassified 1.5 + switch highway=ferry 5.67 + switch highway=bridleway 5 + switch or highway=residential highway=living_street 2 + switch highway=service 2 + switch or highway=track or highway=road highway=path + switch tracktype=grade1 5 + switch ispaved 5 + 30 + 10000 + +---context:node # following code refers to node tags + +# +# calculate logical car access to nodes +# +assign caraccess + switch motorcar= + switch motor_vehicle= + switch vehicle= + switch access= + switch barrier=gate 0 + switch barrier=bollard 0 + switch barrier=lift_gate 0 + switch barrier=cycle_barrier 0 + 1 + or access=yes or access=designated access=destination + or vehicle=yes or vehicle=designated vehicle=destination + or motor_vehicle=yes or motor_vehicle=designated motor_vehicle=destination + or motorcar=yes or motorcar=designated motorcar=destination + +assign initialcost + switch caraccess + 0 + 1000000 diff --git a/brouter-routing-app/src/main/resources/brouter/profiles2/fastbike.brf b/brouter-routing-app/src/main/resources/brouter/profiles2/fastbike.brf new file mode 100644 index 0000000..47d605e --- /dev/null +++ b/brouter-routing-app/src/main/resources/brouter/profiles2/fastbike.brf @@ -0,0 +1,164 @@ +# +# A fastbike could be a racing bike or a speed pedelec. +# But also at night or in rainy whether you might want +# to fallback to this one. +# +# Structure is similar to trekking.brf, see this for documenation. +# + +---context:global # following code refers to global config + +# Use the following switches to change behaviour +# (1=yes, 0=no): + +assign consider_elevation 1 # set to 0 to ignore elevation in routing +assign allow_steps 1 # set to 0 to disallow steps +assign allow_ferries 1 # set to 0 to disallow ferries +assign ignore_cycleroutes 0 # set to 1 for better elevation results +assign stick_to_cycleroutes 0 # set to 1 to just follow cycleroutes +assign avoid_unsafe 0 # set to 1 to avoid standard highways + +# the elevation parameters + +assign downhillcost switch consider_elevation 60 0 +assign downhillcutoff 1.5 +assign uphillcost 0 +assign uphillcutoff 1.5 + +---context:way # following code refers to way-tags + + + +assign ispaved or surface=paved or surface=asphalt or surface=concrete surface=paving_stones +assign isunpaved not or surface= or ispaved or surface=fine_gravel surface=cobblestone + +assign turncost 90 + +assign initialcost switch highway=ferry 10000 0 + +# +# implicit access here just from the motorroad tag +# (implicit access rules from highway tag handled elsewhere) +# +assign defaultaccess + switch access= + not motorroad=yes + switch or access=private access=no + 0 + 1 + +# +# calculate logical bike access +# +assign bikeaccess + or longdistancecycleway=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + not or bicycle=private or bicycle=no bicycle=dismount + +# +# calculate logical foot access +# +assign footaccess + or bikeaccess + or bicycle=dismount + switch foot= + defaultaccess + not or foot=private foot=no + +# +# if not bike-, but foot-acess, just a moderate penalty, +# otherwise access is forbidden +# +assign accesspenalty + switch bikeaccess + 0 + switch footaccess + 6 + 100000 + +# +# handle one-ways. On primary roads, wrong-oneways should +# be close to forbidden, while on other ways we just add +# 4 to the costfactor (making it at least 5 - you are allowed +# to push your bike) +# +assign oneway + switch oneway= + junction=roundabout + or oneway=yes or oneway=true oneway=1 +assign onewaypenalty + switch switch reversedirection=yes oneway oneway=-1 + switch or cycleway=opposite or cycleway=opposite_lane cycleway=opposite_track 0 + switch or highway=primary highway=primary_link 50 + switch or highway=secondary highway=secondary_link 30 + switch or highway=tertiary highway=tertiary_link 20 + 6.0 + 0.0 + +assign costfactor + + add max onewaypenalty accesspenalty + + switch or highway=motorway highway=motorway_link 100000 + switch highway=proposed 100000 + switch or highway=trunk highway=trunk_link 10 + switch or highway=primary highway=primary_link 1.2 + switch or highway=secondary highway=secondary_link 1.1 + switch or highway=tertiary highway=tertiary_link 1.0 + switch highway=unclassified 1.1 + switch highway=pedestrian 10 + switch highway=steps 1000 + switch highway=ferry 5.67 + switch highway=bridleway 5 + switch highway=cycleway 1.3 + switch or highway=residential highway=living_street switch isunpaved 10 1.2 + switch highway=service switch isunpaved 10 1.2 + switch or highway=track or highway=road or highway=path highway=footway + switch tracktype=grade1 switch isunpaved 3 1.2 + switch tracktype=grade2 switch isunpaved 10 3 + switch tracktype=grade3 10.0 + switch tracktype=grade4 20.0 + switch tracktype=grade5 30.0 + switch ispaved 2.0 100.0 + 10.0 + +---context:node # following code refers to node tags + +assign defaultaccess + switch access= + 1 # add default barrier restrictions here! + switch or access=private access=no + 0 + 1 + +assign bikeaccess + or or longdistancecycleway=yes lcn=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + switch or bicycle=private or bicycle=no bicycle=dismount + 0 + 1 + +assign footaccess + or bicycle=dismount + switch foot= + defaultaccess + switch or foot=private foot=no + 0 + 1 + +assign initialcost + switch bikeaccess + 0 + switch footaccess + 300 + 1000000 diff --git a/brouter-routing-app/src/main/resources/brouter/profiles2/lookups.dat b/brouter-routing-app/src/main/resources/brouter/profiles2/lookups.dat new file mode 100644 index 0000000..6bf1a31 --- /dev/null +++ b/brouter-routing-app/src/main/resources/brouter/profiles2/lookups.dat @@ -0,0 +1,317 @@ +---lookupversion:2 + +---context:way + +highway;0001731794 track +highway;0001457935 residential +highway;0000968516 service +highway;0000756237 footway +highway;0000521566 path +highway;0000261772 unclassified +highway;0000220315 secondary +highway;0000207585 tertiary +highway;0000103445 steps +highway;0000102114 primary +highway;0000094484 cycleway +highway;0000090388 living_street +highway;0000035041 motorway +highway;0000029965 pedestrian +highway;0000026875 motorway_link +highway;0000015054 trunk +highway;0000014604 primary_link +highway;0000012211 road +highway;0000011822 trunk_link +highway;0000005882 construction +highway;0000005425 bridleway +highway;0000005180 secondary_link +highway;0000003360 platform +highway;0000002616 proposed abandoned +highway;0000001374 tertiary_link +highway;0000000760 ferry +highway;0000000541 raceway +highway;0000000346 rest_area +highway;0000000300 bus_stop +highway;0000000184 services + +tracktype;0000356503 grade2 +tracktype;0000353482 grade3 +tracktype;0000281625 grade1 +tracktype;0000245193 grade4 +tracktype;0000179135 grade5 + +surface;0000363915 asphalt +surface;0000303589 paved +surface;0000196783 gravel +surface;0000137371 ground +surface;0000128215 grass +surface;0000092748 unpaved +surface;0000086579 paving_stones +surface;0000066111 cobblestone +surface;0000042061 dirt +surface;0000026551 concrete +surface;0000025631 compacted +surface;0000019861 sand +surface;0000009400 pebblestone +surface;0000003197 fine_gravel + +maxspeed;0000402224 30 +maxspeed;0000224685 50 +maxspeed;0000045177 100 +maxspeed;0000037529 70 +maxspeed;0000014237 none +maxspeed;0000014022 60 +maxspeed;0000011530 80 +maxspeed;0000009951 10 +maxspeed;0000008056 20 +maxspeed;0000005772 120 +maxspeed;0000003165 40 +maxspeed;0000002987 7 +maxspeed;0000002826 signals +maxspeed;0000001933 130 + +service;0000221481 parking_aisle +service;0000157110 driveway + +lit;0000132495 yes + +lanes;0000098207 2 +lanes;0000042192 1 +lanes;0000018533 3 +lanes;0000004577 4 +lanes;0000000448 5 +lanes;0000000318 1.5 + +access;0000044859 yes permissive +access;0000008452 designated official +access;0000028727 destination customers +access;0000076985 agricultural forestry +access;0000116270 private +access;0000028044 no + +foot;0000339384 yes allowed Yes +foot;0000125339 designated official +foot;0000018945 no +foot;0000001562 private +foot;0000000279 destination +foot;0000008172 permissive + +bicycle;0000302789 yes allowed permissive +bicycle;0000108056 designated official +bicycle;0000000265 destination +bicycle;0000003593 dismount +bicycle;0000001426 private +bicycle;0000070179 no + +motorcar;0000010111 yes permissive +motorcar;0000001537 designated official +motorcar;0000007102 destination +motorcar;0000016706 agricultural forestry agriculture +motorcar;0000002178 private +motorcar;0000077771 no + +motor_vehicle;0000013813 yes permissive +motor_vehicle;0000002098 designated official +motor_vehicle;0000009792 destination +motor_vehicle;0000019301 agricultural forestry +motor_vehicle;0000006563 private +motor_vehicle;0000025491 no + +motorcycle;0000005750 yes permissive +motorcycle;0000001158 designated official +motorcycle;0000005805 destination +motorcycle;0000012401 agricultural forestry +motorcycle;0000001180 private +motorcycle;0000053955 no + +vehicle;0000000505 yes permissive +vehicle;0000000027 designated +vehicle;0000007582 destination +vehicle;0000004357 agricultural forestry +vehicle;0000001155 private +vehicle;0000006487 no + +cycleway;0000033575 track +cycleway;0000012829 no +cycleway;0000011604 lane +cycleway;0000008938 opposite +cycleway;0000001503 none +cycleway;0000001146 right +cycleway;0000001031 opposite_track +cycleway;0000001029 yes +cycleway;0000000856 opposite_lane +cycleway;0000000675 both +cycleway;0000000665 left +cycleway;0000000521 shared +cycleway;0000000383 street +cycleway;0000000176 segregated + +mtb:scale;0000043968 0 +mtb:scale;0000019705 1 +mtb:scale;0000006436 2 +mtb:scale;0000002702 3 +mtb:scale;0000001083 4 +mtb:scale;0000000329 5 + +sac_scale;0000049626 hiking +sac_scale;0000007933 mountain_hiking +sac_scale;0000001160 demanding_mountain_hiking +sac_scale;0000000523 yes +sac_scale;0000000364 alpine_hiking +sac_scale;0000000117 demanding_alpine_hiking + +noexit;0000058492 yes + +motorroad;0000019250 yes + +oneway;0000330245 yes +oneway;0000075148 no +oneway;0000003679 -1 +oneway;0000000001 true +oneway;0000000001 1 + +junction;0000015929 roundabout + +bridge;0000182649 yes viaduct true suspension + +tunnel;0000031626 yes + +lcn;0000018999 yes + +longdistancecycleway;0000000001 yes + +reversedirection;0000000001 yes + +---context:node + +highway;0000100947 turning_circle +highway;0000067645 traffic_signals +highway;0000047209 crossing +highway;0000037164 bus_stop +highway;0000006577 motorway_junction +highway;0000003811 stop +highway;0000002331 mini_roundabout +highway;0000001789 milestone +highway;0000001692 passing_place +highway;0000001289 give_way +highway;0000001092 emergency_access_point +highway;0000000683 speed_camera +highway;0000000672 steps +highway;0000000658 incline_steep +highway;0000000620 elevator +highway;0000000506 street_lamp +highway;0000000490 ford +highway;0000000458 incline +highway;0000000135 rest_area +highway;0000000105 path +highway;0000000098 emergency_bay +highway;0000000096 road +highway;0000000087 platform +highway;0000000074 services +highway;0000000058 track +highway;0000000055 service +highway;0000000054 footway +highway;0000000053 traffic_calming +highway;0000000046 toll_bridge +highway;0000000037 city_entry + +barrier;0000076979 gate +barrier;0000069308 bollard +barrier;0000028131 lift_gate +barrier;0000017332 cycle_barrier +barrier;0000005693 entrance +barrier;0000002885 block +barrier;0000001065 kissing_gate +barrier;0000000828 cattle_grid +barrier;0000000602 stile +barrier;0000000561 turnstile +barrier;0000000512 no +barrier;0000000463 fence +barrier;0000000417 bump_gate +barrier;0000000324 sally_port +barrier;0000000283 yes +barrier;0000000283 hampshire_gate +barrier;0000000236 swing_gate +barrier;0000000203 chain +barrier;0000000181 toll_booth +barrier;0000000180 door +barrier;0000000104 chicane +barrier;0000000096 tree +barrier;0000000087 border_control +barrier;0000000077 log +barrier;0000000076 traffic_crossing_pole +barrier;0000000063 wall +barrier;0000000060 fallen_tree +barrier;0000000052 stone +barrier;0000000048 ditch +barrier;0000000031 spikes + +access;0000001309 yes permissive +access;0000000118 designated official +access;0000000405 destination customers +access;0000000276 agricultural forestry +access;0000008574 private +access;0000002145 no + +foot;0000080681 yes permissive +foot;0000000326 designated official +foot;0000000023 destination +foot;0000000156 private +foot;0000009170 no + +bicycle;0000076717 yes permissive +bicycle;0000000406 designated official +bicycle;0000000018 destination +bicycle;0000000081 dismount +bicycle;0000000051 private +bicycle;0000016121 no + +motorcar;0000005785 yes permissive +motorcar;0000000026 designated official +motorcar;0000000080 destination +motorcar;0000000112 agricultural forestry +motorcar;0000000171 private +motorcar;0000001817 no + +motor_vehicle;0000000066 yes permissive +motor_vehicle;0000000000 designated official +motor_vehicle;0000000030 destination +motor_vehicle;0000000073 agricultural forestry +motor_vehicle;0000000136 private +motor_vehicle;0000000469 no + +motorcycle;0000004515 yes permissive +motorcycle;0000000007 designated official +motorcycle;0000000054 destination +motorcycle;0000000027 agricultural forestry +motorcycle;0000000063 private +motorcycle;0000001637 no + +vehicle;0000000058 yes permissive +vehicle;0000000000 designated +vehicle;0000000081 destination +vehicle;0000000038 agricultural forestry +vehicle;0000000041 private +vehicle;0000000271 no + +crossing;0000032485 traffic_signals +crossing;0000014300 uncontrolled +crossing;0000005086 island +crossing;0000001565 unmarked +crossing;0000001066 no +crossing;0000000333 zebra + +railway;0000034039 level_crossing +railway;0000010175 crossing + +noexit;0000043010 yes + +entrance;0000015094 yes +entrance;0000007079 main +entrance;0000000554 service +entrance;0000000169 emergency +entrance;0000000063 exit +entrance;0000000008 private + +lcn;0000018999 yes + +longdistancecycleway;0000000001 yes diff --git a/brouter-routing-app/src/main/resources/brouter/profiles2/moped.brf b/brouter-routing-app/src/main/resources/brouter/profiles2/moped.brf new file mode 100644 index 0000000..7343604 --- /dev/null +++ b/brouter-routing-app/src/main/resources/brouter/profiles2/moped.brf @@ -0,0 +1,119 @@ +# +# Moped-Routing is experimantal !!! +# +# DO NOT USE FOR ACTUAL NAVIGATION +# +# Turn restrictions are missing, leading to wrong routes +# + +---context:global + +assign downhillcost 0 +assign downhillcutoff 0 +assign uphillcost 0 +assign uphillcutoff 0 + +---context:way # following code refers to way-tags + +assign turncost 90 +assign initialcost switch highway=ferry 20000 0 + + +# +# calculate logical car access +# +assign motorverhicleaccess + switch motor_vehicle= + switch vehicle= + switch access= + switch or highway=trunk highway=trunk_link 1 + switch or highway=primary highway=primary_link 1 + switch or highway=secondary highway=secondary_link 1 + switch or highway=tertiary highway=tertiary_link 1 + switch highway=unclassified 1 + switch highway=ferry 1 + switch or highway=residential highway=living_street 1 + switch highway=service 1 + 0 + or access=yes or access=designated access=destination + or vehicle=yes or vehicle=designated vehicle=destination + or motor_vehicle=yes or motor_vehicle=designated motor_vehicle=destination + +assign caraccess + switch motorcar= + motorverhicleaccess + or motorcar=yes or motorcar=designated motorcar=destination + +assign motorcycleaccess + switch motorcycle= + motorverhicleaccess + or motorcycle=yes or motorcycle=designated motorcycle=destination + +assign accesspenalty + switch or caraccess motorcycleaccess + switch motorroad=yes 10000 0 + 10000 + +assign onewaypenalty + switch switch reversedirection=yes + switch oneway= + junction=roundabout + or oneway=yes or oneway=true oneway=1 + oneway=-1 + 10000 + 0.0 + + +assign ispaved or surface=paved or surface=asphalt or surface=concrete surface=paving_stones + +assign costfactor + + add max onewaypenalty accesspenalty + + switch or highway=trunk highway=trunk_link 1.5 + switch or highway=primary highway=primary_link switch maxspeed=30 2.0 switch maxspeed=50 1.5 1.2 + switch or highway=secondary highway=secondary_link 1.4 + switch or highway=tertiary highway=tertiary_link 1.3 + switch highway=unclassified 1.2 + switch highway=ferry 5.67 + switch highway=bridleway 5 + switch or highway=residential highway=living_street 2 + switch highway=service 2 + switch or highway=track or highway=road highway=path + switch tracktype=grade1 5 + switch ispaved 5 + 30 + 10000 + +---context:node # following code refers to node tags + +# +# calculate logical car access to nodes +# +assign motorvehicleaccess + switch motor_vehicle= + switch vehicle= + switch access= + switch barrier=gate 0 + switch barrier=bollard 0 + switch barrier=lift_gate 0 + switch barrier=cycle_barrier 0 + 1 + or access=yes or access=designated access=destination + or vehicle=yes or vehicle=designated vehicle=destination + or motor_vehicle=yes or motor_vehicle=designated motor_vehicle=destination + +assign caraccess + switch motorcar= + motorvehicleaccess + or motorcar=yes or motorcar=designated motorcar=destination + +assign motorcycleaccess + switch motorcycle= + motorvehicleaccess + or motorcycle=yes or motorcycle=designated motorcycle=destination + +assign initialcost + switch or caraccess motorcycleaccess + 0 + 1000000 diff --git a/brouter-routing-app/src/main/resources/brouter/profiles2/safety.brf b/brouter-routing-app/src/main/resources/brouter/profiles2/safety.brf new file mode 100644 index 0000000..e759676 --- /dev/null +++ b/brouter-routing-app/src/main/resources/brouter/profiles2/safety.brf @@ -0,0 +1,223 @@ +# *** The trekking profile is for slow travel +# *** and avoiding car traffic, but still with +# *** a focus on approaching your destination +# *** efficiently. + +---context:global # following code refers to global config + +# Use the following switches to change behaviour +# (1=yes, 0=no): + +assign consider_elevation 1 # set to 0 to ignore elevation in routing +assign allow_steps 1 # set to 0 to disallow steps +assign allow_ferries 1 # set to 0 to disallow ferries +assign ignore_cycleroutes 0 # set to 1 for better elevation results +assign stick_to_cycleroutes 0 # set to 1 to just follow cycleroutes +assign avoid_unsafe 1 # set to 1 to avoid standard highways + +# the elevation parameters + +assign downhillcost switch consider_elevation 60 0 +assign downhillcutoff 1.5 +assign uphillcost 0 +assign uphillcutoff 1.5 + +---context:way # following code refers to way-tags + +# +# pre-calculate some logical expressions +# +assign is_ldcr and longdistancecycleway=yes not ignore_cycleroutes +assign isbike or bicycle=yes or bicycle=designated lcn=yes +assign ispaved or surface=paved or surface=asphalt or surface=concrete surface=paving_stones +assign isunpaved not or surface= or ispaved or surface=fine_gravel surface=cobblestone +assign probablyGood or ispaved and isbike not isunpaved + + +# +# this is the cost (in Meter) for a 90-degree turn +# The actual cost is calculated as turncost*cos(angle) +# (Suppressing turncost while following longdistance-cycleways +# makes them a little bit more magnetic) +# +assign turncost switch is_ldcr 0 90 + +# +# calculate the initial cost +# this is added to the total cost each time the costfactor +# changed +# +assign initialcost switch highway=ferry 10000 0 + +# +# implicit access here just from the motorroad tag +# (implicit access rules from highway tag handled elsewhere) +# +assign defaultaccess + switch access= + not motorroad=yes + switch or access=private access=no + 0 + 1 + +# +# calculate logical bike access +# +assign bikeaccess + or longdistancecycleway=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + not or bicycle=private or bicycle=no bicycle=dismount + +# +# calculate logical foot access +# +assign footaccess + or bikeaccess + or bicycle=dismount + switch foot= + defaultaccess + not or foot=private foot=no + +# +# if not bike-, but foot-acess, just a moderate penalty, +# otherwise access is forbidden +# +assign accesspenalty + switch bikeaccess + 0 + switch footaccess + 4 + 100000 + +# +# handle one-ways. On primary roads, wrong-oneways should +# be close to forbidden, while on other ways we just add +# 4 to the costfactor (making it at least 5 - you are allowed +# to push your bike) +# +assign oneway + switch oneway= + junction=roundabout + or oneway=yes or oneway=true oneway=1 +assign onewaypenalty + switch switch reversedirection=yes oneway oneway=-1 + switch or cycleway=opposite or cycleway=opposite_lane cycleway=opposite_track 0 + switch or highway=primary highway=primary_link 50 + switch or highway=secondary highway=secondary_link 30 + switch or highway=tertiary highway=tertiary_link 20 + 4.0 + 0.0 + +# +# calculate the cost-factor, which is the factor +# by which the distance of a way-segment is multiplied +# to calculate the cost of that segment. The costfactor +# must be >=1 and it's supposed to be close to 1 for +# the type of way the routing profile is searching for +# +assign costfactor + + add max onewaypenalty accesspenalty + + # + # steps and ferries are special. Note this is handled + # before the longdistancecycleway-switch, to be able + # to really exlude them be setting cost to infinity + # + switch highway=steps switch allow_steps 40 100000 + switch highway=ferry switch allow_ferries 5.67 100000 + + # + # handle long-distance cycle-routes. + # + switch is_ldcr 1 # always treated as perfect (=1) + add switch stick_to_cycleroutes 0.5 0.05 # everything else somewhat up + + # + # some other highway types + # + switch highway=pedestrian 3 + switch highway=bridleway 5 + switch highway=cycleway 1 + switch or highway=residential highway=living_street switch isunpaved 1.5 1.1 + switch highway=service switch isunpaved 1.6 1.3 + + # + # tracks and track-like ways are rated mainly be tracktype/grade + # But note that if no tracktype is given (mainly for road/path/footway) + # it can be o.k. if there's any other hint for quality + # + switch or highway=track or highway=road or highway=path highway=footway + switch tracktype=grade1 switch probablyGood 1.0 1.3 + switch tracktype=grade2 switch probablyGood 1.1 2.0 + switch tracktype=grade3 switch probablyGood 1.5 3.0 + switch tracktype=grade4 switch probablyGood 2.0 5.0 + switch tracktype=grade5 switch probablyGood 3.0 5.0 + switch probablyGood 1.0 5.0 + + # + # When avoiding unsafe ways, avoid highways without a bike hint + # + add switch and avoid_unsafe not isbike 2 0 + + # + # exclude motorways and proposed roads + # + switch or highway=motorway highway=motorway_link 100000 + switch highway=proposed 100000 + + # + # actuals roads are o.k. if we have a bike hint + # + switch or highway=trunk highway=trunk_link switch isbike 1.5 10 + switch or highway=primary highway=primary_link switch isbike 1.2 3 + switch or highway=secondary highway=secondary_link switch isbike 1.1 1.6 + switch or highway=tertiary highway=tertiary_link switch isbike 1.0 1.4 + switch highway=unclassified switch isbike 1.0 1.3 + + # + # default for any other highway type not handled above + # + 2.0 + + +---context:node # following code refers to node tags + +assign defaultaccess + switch access= + 1 # add default barrier restrictions here! + switch or access=private access=no + 0 + 1 + +assign bikeaccess + or or longdistancecycleway=yes lcn=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + switch or bicycle=private or bicycle=no bicycle=dismount + 0 + 1 + +assign footaccess + or bicycle=dismount + switch foot= + defaultaccess + switch or foot=private foot=no + 0 + 1 + +assign initialcost + switch bikeaccess + 0 + switch footaccess + 100 + 1000000 diff --git a/brouter-routing-app/src/main/resources/brouter/profiles2/shortest.brf b/brouter-routing-app/src/main/resources/brouter/profiles2/shortest.brf new file mode 100644 index 0000000..000b506 --- /dev/null +++ b/brouter-routing-app/src/main/resources/brouter/profiles2/shortest.brf @@ -0,0 +1,89 @@ +---context:global # following code refers to global config + +# the elevation parameters + +assign downhillcost 0 +assign downhillcutoff 1.5 +assign uphillcost 0 +assign uphillcutoff 1.5 + +---context:way # following code refers to way-tags + +assign turncost 0 + +assign initialcost switch highway=ferry 10000 0 + +# +# implicit access here just from the motorroad tag +# (implicit access rules from highway tag handled elsewhere) +# +assign defaultaccess + switch access= + not motorroad=yes + switch or access=private access=no + 0 + 1 + +# +# calculate logical bike access +# +assign bikeaccess + or longdistancecycleway=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + not or bicycle=private or bicycle=no bicycle=dismount + +# +# calculate logical foot access +# +assign footaccess + or bikeaccess + or bicycle=dismount + switch foot= + defaultaccess + not or foot=private foot=no + +assign accesspenalty switch or bikeaccess footaccess 0 100000 + +assign costfactor + add accesspenalty + + switch highway=ferry 5.67 + switch or highway=motorway highway=motorway_link 100000 + switch highway=proposed 100000 + 1 + +---context:node # following code refers to node tags + +assign defaultaccess + switch access= + 1 # add default barrier restrictions here! + switch or access=private access=no + 0 + 1 + +assign bikeaccess + or or longdistancecycleway=yes lcn=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + switch or bicycle=private or bicycle=no bicycle=dismount + 0 + 1 + +assign footaccess + or bicycle=dismount + switch foot= + defaultaccess + switch or foot=private foot=no + 0 + 1 + +assign initialcost switch or bikeaccess footaccess 0 1000000 diff --git a/brouter-routing-app/src/main/resources/brouter/profiles2/trekking-ignore-cr.brf b/brouter-routing-app/src/main/resources/brouter/profiles2/trekking-ignore-cr.brf new file mode 100644 index 0000000..8639f46 --- /dev/null +++ b/brouter-routing-app/src/main/resources/brouter/profiles2/trekking-ignore-cr.brf @@ -0,0 +1,223 @@ +# *** The trekking profile is for slow travel +# *** and avoiding car traffic, but still with +# *** a focus on approaching your destination +# *** efficiently. + +---context:global # following code refers to global config + +# Use the following switches to change behaviour +# (1=yes, 0=no): + +assign consider_elevation 1 # set to 0 to ignore elevation in routing +assign allow_steps 1 # set to 0 to disallow steps +assign allow_ferries 1 # set to 0 to disallow ferries +assign ignore_cycleroutes 1 # set to 1 for better elevation results +assign stick_to_cycleroutes 0 # set to 1 to just follow cycleroutes +assign avoid_unsafe 0 # set to 1 to avoid standard highways + +# the elevation parameters + +assign downhillcost switch consider_elevation 60 0 +assign downhillcutoff 1.5 +assign uphillcost 0 +assign uphillcutoff 1.5 + +---context:way # following code refers to way-tags + +# +# pre-calculate some logical expressions +# +assign is_ldcr and longdistancecycleway=yes not ignore_cycleroutes +assign isbike or bicycle=yes or bicycle=designated lcn=yes +assign ispaved or surface=paved or surface=asphalt or surface=concrete surface=paving_stones +assign isunpaved not or surface= or ispaved or surface=fine_gravel surface=cobblestone +assign probablyGood or ispaved and isbike not isunpaved + + +# +# this is the cost (in Meter) for a 90-degree turn +# The actual cost is calculated as turncost*cos(angle) +# (Suppressing turncost while following longdistance-cycleways +# makes them a little bit more magnetic) +# +assign turncost switch is_ldcr 0 90 + +# +# calculate the initial cost +# this is added to the total cost each time the costfactor +# changed +# +assign initialcost switch highway=ferry 10000 0 + +# +# implicit access here just from the motorroad tag +# (implicit access rules from highway tag handled elsewhere) +# +assign defaultaccess + switch access= + not motorroad=yes + switch or access=private access=no + 0 + 1 + +# +# calculate logical bike access +# +assign bikeaccess + or longdistancecycleway=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + not or bicycle=private or bicycle=no bicycle=dismount + +# +# calculate logical foot access +# +assign footaccess + or bikeaccess + or bicycle=dismount + switch foot= + defaultaccess + not or foot=private foot=no + +# +# if not bike-, but foot-acess, just a moderate penalty, +# otherwise access is forbidden +# +assign accesspenalty + switch bikeaccess + 0 + switch footaccess + 4 + 100000 + +# +# handle one-ways. On primary roads, wrong-oneways should +# be close to forbidden, while on other ways we just add +# 4 to the costfactor (making it at least 5 - you are allowed +# to push your bike) +# +assign oneway + switch oneway= + junction=roundabout + or oneway=yes or oneway=true oneway=1 +assign onewaypenalty + switch switch reversedirection=yes oneway oneway=-1 + switch or cycleway=opposite or cycleway=opposite_lane cycleway=opposite_track 0 + switch or highway=primary highway=primary_link 50 + switch or highway=secondary highway=secondary_link 30 + switch or highway=tertiary highway=tertiary_link 20 + 4.0 + 0.0 + +# +# calculate the cost-factor, which is the factor +# by which the distance of a way-segment is multiplied +# to calculate the cost of that segment. The costfactor +# must be >=1 and it's supposed to be close to 1 for +# the type of way the routing profile is searching for +# +assign costfactor + + add max onewaypenalty accesspenalty + + # + # steps and ferries are special. Note this is handled + # before the longdistancecycleway-switch, to be able + # to really exlude them be setting cost to infinity + # + switch highway=steps switch allow_steps 40 100000 + switch highway=ferry switch allow_ferries 5.67 100000 + + # + # handle long-distance cycle-routes. + # + switch is_ldcr 1 # always treated as perfect (=1) + add switch stick_to_cycleroutes 0.5 0.05 # everything else somewhat up + + # + # some other highway types + # + switch highway=pedestrian 3 + switch highway=bridleway 5 + switch highway=cycleway 1 + switch or highway=residential highway=living_street switch isunpaved 1.5 1.1 + switch highway=service switch isunpaved 1.6 1.3 + + # + # tracks and track-like ways are rated mainly be tracktype/grade + # But note that if no tracktype is given (mainly for road/path/footway) + # it can be o.k. if there's any other hint for quality + # + switch or highway=track or highway=road or highway=path highway=footway + switch tracktype=grade1 switch probablyGood 1.0 1.3 + switch tracktype=grade2 switch probablyGood 1.1 2.0 + switch tracktype=grade3 switch probablyGood 1.5 3.0 + switch tracktype=grade4 switch probablyGood 2.0 5.0 + switch tracktype=grade5 switch probablyGood 3.0 5.0 + switch probablyGood 1.0 5.0 + + # + # When avoiding unsafe ways, avoid highways without a bike hint + # + add switch and avoid_unsafe not isbike 2 0 + + # + # exclude motorways and proposed roads + # + switch or highway=motorway highway=motorway_link 100000 + switch highway=proposed 100000 + + # + # actuals roads are o.k. if we have a bike hint + # + switch or highway=trunk highway=trunk_link switch isbike 1.5 10 + switch or highway=primary highway=primary_link switch isbike 1.2 3 + switch or highway=secondary highway=secondary_link switch isbike 1.1 1.6 + switch or highway=tertiary highway=tertiary_link switch isbike 1.0 1.4 + switch highway=unclassified switch isbike 1.0 1.3 + + # + # default for any other highway type not handled above + # + 2.0 + + +---context:node # following code refers to node tags + +assign defaultaccess + switch access= + 1 # add default barrier restrictions here! + switch or access=private access=no + 0 + 1 + +assign bikeaccess + or or longdistancecycleway=yes lcn=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + switch or bicycle=private or bicycle=no bicycle=dismount + 0 + 1 + +assign footaccess + or bicycle=dismount + switch foot= + defaultaccess + switch or foot=private foot=no + 0 + 1 + +assign initialcost + switch bikeaccess + 0 + switch footaccess + 100 + 1000000 diff --git a/brouter-routing-app/src/main/resources/brouter/profiles2/trekking-noferries.brf b/brouter-routing-app/src/main/resources/brouter/profiles2/trekking-noferries.brf new file mode 100644 index 0000000..ea1dba9 --- /dev/null +++ b/brouter-routing-app/src/main/resources/brouter/profiles2/trekking-noferries.brf @@ -0,0 +1,223 @@ +# *** The trekking profile is for slow travel +# *** and avoiding car traffic, but still with +# *** a focus on approaching your destination +# *** efficiently. + +---context:global # following code refers to global config + +# Use the following switches to change behaviour +# (1=yes, 0=no): + +assign consider_elevation 1 # set to 0 to ignore elevation in routing +assign allow_steps 1 # set to 0 to disallow steps +assign allow_ferries 0 # set to 0 to disallow ferries +assign ignore_cycleroutes 0 # set to 1 for better elevation results +assign stick_to_cycleroutes 0 # set to 1 to just follow cycleroutes +assign avoid_unsafe 0 # set to 1 to avoid standard highways + +# the elevation parameters + +assign downhillcost switch consider_elevation 60 0 +assign downhillcutoff 1.5 +assign uphillcost 0 +assign uphillcutoff 1.5 + +---context:way # following code refers to way-tags + +# +# pre-calculate some logical expressions +# +assign is_ldcr and longdistancecycleway=yes not ignore_cycleroutes +assign isbike or bicycle=yes or bicycle=designated lcn=yes +assign ispaved or surface=paved or surface=asphalt or surface=concrete surface=paving_stones +assign isunpaved not or surface= or ispaved or surface=fine_gravel surface=cobblestone +assign probablyGood or ispaved and isbike not isunpaved + + +# +# this is the cost (in Meter) for a 90-degree turn +# The actual cost is calculated as turncost*cos(angle) +# (Suppressing turncost while following longdistance-cycleways +# makes them a little bit more magnetic) +# +assign turncost switch is_ldcr 0 90 + +# +# calculate the initial cost +# this is added to the total cost each time the costfactor +# changed +# +assign initialcost switch highway=ferry 10000 0 + +# +# implicit access here just from the motorroad tag +# (implicit access rules from highway tag handled elsewhere) +# +assign defaultaccess + switch access= + not motorroad=yes + switch or access=private access=no + 0 + 1 + +# +# calculate logical bike access +# +assign bikeaccess + or longdistancecycleway=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + not or bicycle=private or bicycle=no bicycle=dismount + +# +# calculate logical foot access +# +assign footaccess + or bikeaccess + or bicycle=dismount + switch foot= + defaultaccess + not or foot=private foot=no + +# +# if not bike-, but foot-acess, just a moderate penalty, +# otherwise access is forbidden +# +assign accesspenalty + switch bikeaccess + 0 + switch footaccess + 4 + 100000 + +# +# handle one-ways. On primary roads, wrong-oneways should +# be close to forbidden, while on other ways we just add +# 4 to the costfactor (making it at least 5 - you are allowed +# to push your bike) +# +assign oneway + switch oneway= + junction=roundabout + or oneway=yes or oneway=true oneway=1 +assign onewaypenalty + switch switch reversedirection=yes oneway oneway=-1 + switch or cycleway=opposite or cycleway=opposite_lane cycleway=opposite_track 0 + switch or highway=primary highway=primary_link 50 + switch or highway=secondary highway=secondary_link 30 + switch or highway=tertiary highway=tertiary_link 20 + 4.0 + 0.0 + +# +# calculate the cost-factor, which is the factor +# by which the distance of a way-segment is multiplied +# to calculate the cost of that segment. The costfactor +# must be >=1 and it's supposed to be close to 1 for +# the type of way the routing profile is searching for +# +assign costfactor + + add max onewaypenalty accesspenalty + + # + # steps and ferries are special. Note this is handled + # before the longdistancecycleway-switch, to be able + # to really exlude them be setting cost to infinity + # + switch highway=steps switch allow_steps 40 100000 + switch highway=ferry switch allow_ferries 5.67 100000 + + # + # handle long-distance cycle-routes. + # + switch is_ldcr 1 # always treated as perfect (=1) + add switch stick_to_cycleroutes 0.5 0.05 # everything else somewhat up + + # + # some other highway types + # + switch highway=pedestrian 3 + switch highway=bridleway 5 + switch highway=cycleway 1 + switch or highway=residential highway=living_street switch isunpaved 1.5 1.1 + switch highway=service switch isunpaved 1.6 1.3 + + # + # tracks and track-like ways are rated mainly be tracktype/grade + # But note that if no tracktype is given (mainly for road/path/footway) + # it can be o.k. if there's any other hint for quality + # + switch or highway=track or highway=road or highway=path highway=footway + switch tracktype=grade1 switch probablyGood 1.0 1.3 + switch tracktype=grade2 switch probablyGood 1.1 2.0 + switch tracktype=grade3 switch probablyGood 1.5 3.0 + switch tracktype=grade4 switch probablyGood 2.0 5.0 + switch tracktype=grade5 switch probablyGood 3.0 5.0 + switch probablyGood 1.0 5.0 + + # + # When avoiding unsafe ways, avoid highways without a bike hint + # + add switch and avoid_unsafe not isbike 2 0 + + # + # exclude motorways and proposed roads + # + switch or highway=motorway highway=motorway_link 100000 + switch highway=proposed 100000 + + # + # actuals roads are o.k. if we have a bike hint + # + switch or highway=trunk highway=trunk_link switch isbike 1.5 10 + switch or highway=primary highway=primary_link switch isbike 1.2 3 + switch or highway=secondary highway=secondary_link switch isbike 1.1 1.6 + switch or highway=tertiary highway=tertiary_link switch isbike 1.0 1.4 + switch highway=unclassified switch isbike 1.0 1.3 + + # + # default for any other highway type not handled above + # + 2.0 + + +---context:node # following code refers to node tags + +assign defaultaccess + switch access= + 1 # add default barrier restrictions here! + switch or access=private access=no + 0 + 1 + +assign bikeaccess + or or longdistancecycleway=yes lcn=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + switch or bicycle=private or bicycle=no bicycle=dismount + 0 + 1 + +assign footaccess + or bicycle=dismount + switch foot= + defaultaccess + switch or foot=private foot=no + 0 + 1 + +assign initialcost + switch bikeaccess + 0 + switch footaccess + 100 + 1000000 diff --git a/brouter-routing-app/src/main/resources/brouter/profiles2/trekking-nosteps.brf b/brouter-routing-app/src/main/resources/brouter/profiles2/trekking-nosteps.brf new file mode 100644 index 0000000..1a70dcd --- /dev/null +++ b/brouter-routing-app/src/main/resources/brouter/profiles2/trekking-nosteps.brf @@ -0,0 +1,223 @@ +# *** The trekking profile is for slow travel +# *** and avoiding car traffic, but still with +# *** a focus on approaching your destination +# *** efficiently. + +---context:global # following code refers to global config + +# Use the following switches to change behaviour +# (1=yes, 0=no): + +assign consider_elevation 1 # set to 0 to ignore elevation in routing +assign allow_steps 0 # set to 0 to disallow steps +assign allow_ferries 1 # set to 0 to disallow ferries +assign ignore_cycleroutes 0 # set to 1 for better elevation results +assign stick_to_cycleroutes 0 # set to 1 to just follow cycleroutes +assign avoid_unsafe 0 # set to 1 to avoid standard highways + +# the elevation parameters + +assign downhillcost switch consider_elevation 60 0 +assign downhillcutoff 1.5 +assign uphillcost 0 +assign uphillcutoff 1.5 + +---context:way # following code refers to way-tags + +# +# pre-calculate some logical expressions +# +assign is_ldcr and longdistancecycleway=yes not ignore_cycleroutes +assign isbike or bicycle=yes or bicycle=designated lcn=yes +assign ispaved or surface=paved or surface=asphalt or surface=concrete surface=paving_stones +assign isunpaved not or surface= or ispaved or surface=fine_gravel surface=cobblestone +assign probablyGood or ispaved and isbike not isunpaved + + +# +# this is the cost (in Meter) for a 90-degree turn +# The actual cost is calculated as turncost*cos(angle) +# (Suppressing turncost while following longdistance-cycleways +# makes them a little bit more magnetic) +# +assign turncost switch is_ldcr 0 90 + +# +# calculate the initial cost +# this is added to the total cost each time the costfactor +# changed +# +assign initialcost switch highway=ferry 10000 0 + +# +# implicit access here just from the motorroad tag +# (implicit access rules from highway tag handled elsewhere) +# +assign defaultaccess + switch access= + not motorroad=yes + switch or access=private access=no + 0 + 1 + +# +# calculate logical bike access +# +assign bikeaccess + or longdistancecycleway=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + not or bicycle=private or bicycle=no bicycle=dismount + +# +# calculate logical foot access +# +assign footaccess + or bikeaccess + or bicycle=dismount + switch foot= + defaultaccess + not or foot=private foot=no + +# +# if not bike-, but foot-acess, just a moderate penalty, +# otherwise access is forbidden +# +assign accesspenalty + switch bikeaccess + 0 + switch footaccess + 4 + 100000 + +# +# handle one-ways. On primary roads, wrong-oneways should +# be close to forbidden, while on other ways we just add +# 4 to the costfactor (making it at least 5 - you are allowed +# to push your bike) +# +assign oneway + switch oneway= + junction=roundabout + or oneway=yes or oneway=true oneway=1 +assign onewaypenalty + switch switch reversedirection=yes oneway oneway=-1 + switch or cycleway=opposite or cycleway=opposite_lane cycleway=opposite_track 0 + switch or highway=primary highway=primary_link 50 + switch or highway=secondary highway=secondary_link 30 + switch or highway=tertiary highway=tertiary_link 20 + 4.0 + 0.0 + +# +# calculate the cost-factor, which is the factor +# by which the distance of a way-segment is multiplied +# to calculate the cost of that segment. The costfactor +# must be >=1 and it's supposed to be close to 1 for +# the type of way the routing profile is searching for +# +assign costfactor + + add max onewaypenalty accesspenalty + + # + # steps and ferries are special. Note this is handled + # before the longdistancecycleway-switch, to be able + # to really exlude them be setting cost to infinity + # + switch highway=steps switch allow_steps 40 100000 + switch highway=ferry switch allow_ferries 5.67 100000 + + # + # handle long-distance cycle-routes. + # + switch is_ldcr 1 # always treated as perfect (=1) + add switch stick_to_cycleroutes 0.5 0.05 # everything else somewhat up + + # + # some other highway types + # + switch highway=pedestrian 3 + switch highway=bridleway 5 + switch highway=cycleway 1 + switch or highway=residential highway=living_street switch isunpaved 1.5 1.1 + switch highway=service switch isunpaved 1.6 1.3 + + # + # tracks and track-like ways are rated mainly be tracktype/grade + # But note that if no tracktype is given (mainly for road/path/footway) + # it can be o.k. if there's any other hint for quality + # + switch or highway=track or highway=road or highway=path highway=footway + switch tracktype=grade1 switch probablyGood 1.0 1.3 + switch tracktype=grade2 switch probablyGood 1.1 2.0 + switch tracktype=grade3 switch probablyGood 1.5 3.0 + switch tracktype=grade4 switch probablyGood 2.0 5.0 + switch tracktype=grade5 switch probablyGood 3.0 5.0 + switch probablyGood 1.0 5.0 + + # + # When avoiding unsafe ways, avoid highways without a bike hint + # + add switch and avoid_unsafe not isbike 2 0 + + # + # exclude motorways and proposed roads + # + switch or highway=motorway highway=motorway_link 100000 + switch highway=proposed 100000 + + # + # actuals roads are o.k. if we have a bike hint + # + switch or highway=trunk highway=trunk_link switch isbike 1.5 10 + switch or highway=primary highway=primary_link switch isbike 1.2 3 + switch or highway=secondary highway=secondary_link switch isbike 1.1 1.6 + switch or highway=tertiary highway=tertiary_link switch isbike 1.0 1.4 + switch highway=unclassified switch isbike 1.0 1.3 + + # + # default for any other highway type not handled above + # + 2.0 + + +---context:node # following code refers to node tags + +assign defaultaccess + switch access= + 1 # add default barrier restrictions here! + switch or access=private access=no + 0 + 1 + +assign bikeaccess + or or longdistancecycleway=yes lcn=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + switch or bicycle=private or bicycle=no bicycle=dismount + 0 + 1 + +assign footaccess + or bicycle=dismount + switch foot= + defaultaccess + switch or foot=private foot=no + 0 + 1 + +assign initialcost + switch bikeaccess + 0 + switch footaccess + 100 + 1000000 diff --git a/brouter-routing-app/src/main/resources/brouter/profiles2/trekking-steep.brf b/brouter-routing-app/src/main/resources/brouter/profiles2/trekking-steep.brf new file mode 100644 index 0000000..822b1ec --- /dev/null +++ b/brouter-routing-app/src/main/resources/brouter/profiles2/trekking-steep.brf @@ -0,0 +1,223 @@ +# *** The trekking profile is for slow travel +# *** and avoiding car traffic, but still with +# *** a focus on approaching your destination +# *** efficiently. + +---context:global # following code refers to global config + +# Use the following switches to change behaviour +# (1=yes, 0=no): + +assign consider_elevation 0 # set to 0 to ignore elevation in routing +assign allow_steps 1 # set to 0 to disallow steps +assign allow_ferries 1 # set to 0 to disallow ferries +assign ignore_cycleroutes 0 # set to 1 for better elevation results +assign stick_to_cycleroutes 0 # set to 1 to just follow cycleroutes +assign avoid_unsafe 0 # set to 1 to avoid standard highways + +# the elevation parameters + +assign downhillcost switch consider_elevation 60 0 +assign downhillcutoff 1.5 +assign uphillcost 0 +assign uphillcutoff 1.5 + +---context:way # following code refers to way-tags + +# +# pre-calculate some logical expressions +# +assign is_ldcr and longdistancecycleway=yes not ignore_cycleroutes +assign isbike or bicycle=yes or bicycle=designated lcn=yes +assign ispaved or surface=paved or surface=asphalt or surface=concrete surface=paving_stones +assign isunpaved not or surface= or ispaved or surface=fine_gravel surface=cobblestone +assign probablyGood or ispaved and isbike not isunpaved + + +# +# this is the cost (in Meter) for a 90-degree turn +# The actual cost is calculated as turncost*cos(angle) +# (Suppressing turncost while following longdistance-cycleways +# makes them a little bit more magnetic) +# +assign turncost switch is_ldcr 0 90 + +# +# calculate the initial cost +# this is added to the total cost each time the costfactor +# changed +# +assign initialcost switch highway=ferry 10000 0 + +# +# implicit access here just from the motorroad tag +# (implicit access rules from highway tag handled elsewhere) +# +assign defaultaccess + switch access= + not motorroad=yes + switch or access=private access=no + 0 + 1 + +# +# calculate logical bike access +# +assign bikeaccess + or longdistancecycleway=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + not or bicycle=private or bicycle=no bicycle=dismount + +# +# calculate logical foot access +# +assign footaccess + or bikeaccess + or bicycle=dismount + switch foot= + defaultaccess + not or foot=private foot=no + +# +# if not bike-, but foot-acess, just a moderate penalty, +# otherwise access is forbidden +# +assign accesspenalty + switch bikeaccess + 0 + switch footaccess + 4 + 100000 + +# +# handle one-ways. On primary roads, wrong-oneways should +# be close to forbidden, while on other ways we just add +# 4 to the costfactor (making it at least 5 - you are allowed +# to push your bike) +# +assign oneway + switch oneway= + junction=roundabout + or oneway=yes or oneway=true oneway=1 +assign onewaypenalty + switch switch reversedirection=yes oneway oneway=-1 + switch or cycleway=opposite or cycleway=opposite_lane cycleway=opposite_track 0 + switch or highway=primary highway=primary_link 50 + switch or highway=secondary highway=secondary_link 30 + switch or highway=tertiary highway=tertiary_link 20 + 4.0 + 0.0 + +# +# calculate the cost-factor, which is the factor +# by which the distance of a way-segment is multiplied +# to calculate the cost of that segment. The costfactor +# must be >=1 and it's supposed to be close to 1 for +# the type of way the routing profile is searching for +# +assign costfactor + + add max onewaypenalty accesspenalty + + # + # steps and ferries are special. Note this is handled + # before the longdistancecycleway-switch, to be able + # to really exlude them be setting cost to infinity + # + switch highway=steps switch allow_steps 40 100000 + switch highway=ferry switch allow_ferries 5.67 100000 + + # + # handle long-distance cycle-routes. + # + switch is_ldcr 1 # always treated as perfect (=1) + add switch stick_to_cycleroutes 0.5 0.05 # everything else somewhat up + + # + # some other highway types + # + switch highway=pedestrian 3 + switch highway=bridleway 5 + switch highway=cycleway 1 + switch or highway=residential highway=living_street switch isunpaved 1.5 1.1 + switch highway=service switch isunpaved 1.6 1.3 + + # + # tracks and track-like ways are rated mainly be tracktype/grade + # But note that if no tracktype is given (mainly for road/path/footway) + # it can be o.k. if there's any other hint for quality + # + switch or highway=track or highway=road or highway=path highway=footway + switch tracktype=grade1 switch probablyGood 1.0 1.3 + switch tracktype=grade2 switch probablyGood 1.1 2.0 + switch tracktype=grade3 switch probablyGood 1.5 3.0 + switch tracktype=grade4 switch probablyGood 2.0 5.0 + switch tracktype=grade5 switch probablyGood 3.0 5.0 + switch probablyGood 1.0 5.0 + + # + # When avoiding unsafe ways, avoid highways without a bike hint + # + add switch and avoid_unsafe not isbike 2 0 + + # + # exclude motorways and proposed roads + # + switch or highway=motorway highway=motorway_link 100000 + switch highway=proposed 100000 + + # + # actuals roads are o.k. if we have a bike hint + # + switch or highway=trunk highway=trunk_link switch isbike 1.5 10 + switch or highway=primary highway=primary_link switch isbike 1.2 3 + switch or highway=secondary highway=secondary_link switch isbike 1.1 1.6 + switch or highway=tertiary highway=tertiary_link switch isbike 1.0 1.4 + switch highway=unclassified switch isbike 1.0 1.3 + + # + # default for any other highway type not handled above + # + 2.0 + + +---context:node # following code refers to node tags + +assign defaultaccess + switch access= + 1 # add default barrier restrictions here! + switch or access=private access=no + 0 + 1 + +assign bikeaccess + or or longdistancecycleway=yes lcn=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + switch or bicycle=private or bicycle=no bicycle=dismount + 0 + 1 + +assign footaccess + or bicycle=dismount + switch foot= + defaultaccess + switch or foot=private foot=no + 0 + 1 + +assign initialcost + switch bikeaccess + 0 + switch footaccess + 100 + 1000000 diff --git a/brouter-routing-app/src/main/resources/brouter/profiles2/trekking.brf b/brouter-routing-app/src/main/resources/brouter/profiles2/trekking.brf new file mode 100644 index 0000000..ec2940e --- /dev/null +++ b/brouter-routing-app/src/main/resources/brouter/profiles2/trekking.brf @@ -0,0 +1,223 @@ +# *** The trekking profile is for slow travel +# *** and avoiding car traffic, but still with +# *** a focus on approaching your destination +# *** efficiently. + +---context:global # following code refers to global config + +# Use the following switches to change behaviour +# (1=yes, 0=no): + +assign consider_elevation 1 # set to 0 to ignore elevation in routing +assign allow_steps 1 # set to 0 to disallow steps +assign allow_ferries 1 # set to 0 to disallow ferries +assign ignore_cycleroutes 0 # set to 1 for better elevation results +assign stick_to_cycleroutes 0 # set to 1 to just follow cycleroutes +assign avoid_unsafe 0 # set to 1 to avoid standard highways + +# the elevation parameters + +assign downhillcost switch consider_elevation 60 0 +assign downhillcutoff 1.5 +assign uphillcost 0 +assign uphillcutoff 1.5 + +---context:way # following code refers to way-tags + +# +# pre-calculate some logical expressions +# +assign is_ldcr and longdistancecycleway=yes not ignore_cycleroutes +assign isbike or bicycle=yes or bicycle=designated lcn=yes +assign ispaved or surface=paved or surface=asphalt or surface=concrete surface=paving_stones +assign isunpaved not or surface= or ispaved or surface=fine_gravel surface=cobblestone +assign probablyGood or ispaved and isbike not isunpaved + + +# +# this is the cost (in Meter) for a 90-degree turn +# The actual cost is calculated as turncost*cos(angle) +# (Suppressing turncost while following longdistance-cycleways +# makes them a little bit more magnetic) +# +assign turncost switch is_ldcr 0 90 + +# +# calculate the initial cost +# this is added to the total cost each time the costfactor +# changed +# +assign initialcost switch highway=ferry 10000 0 + +# +# implicit access here just from the motorroad tag +# (implicit access rules from highway tag handled elsewhere) +# +assign defaultaccess + switch access= + not motorroad=yes + switch or access=private access=no + 0 + 1 + +# +# calculate logical bike access +# +assign bikeaccess + or longdistancecycleway=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + not or bicycle=private or bicycle=no bicycle=dismount + +# +# calculate logical foot access +# +assign footaccess + or bikeaccess + or bicycle=dismount + switch foot= + defaultaccess + not or foot=private foot=no + +# +# if not bike-, but foot-acess, just a moderate penalty, +# otherwise access is forbidden +# +assign accesspenalty + switch bikeaccess + 0 + switch footaccess + 4 + 100000 + +# +# handle one-ways. On primary roads, wrong-oneways should +# be close to forbidden, while on other ways we just add +# 4 to the costfactor (making it at least 5 - you are allowed +# to push your bike) +# +assign oneway + switch oneway= + junction=roundabout + or oneway=yes or oneway=true oneway=1 +assign onewaypenalty + switch switch reversedirection=yes oneway oneway=-1 + switch or cycleway=opposite or cycleway=opposite_lane cycleway=opposite_track 0 + switch or highway=primary highway=primary_link 50 + switch or highway=secondary highway=secondary_link 30 + switch or highway=tertiary highway=tertiary_link 20 + 4.0 + 0.0 + +# +# calculate the cost-factor, which is the factor +# by which the distance of a way-segment is multiplied +# to calculate the cost of that segment. The costfactor +# must be >=1 and it's supposed to be close to 1 for +# the type of way the routing profile is searching for +# +assign costfactor + + add max onewaypenalty accesspenalty + + # + # steps and ferries are special. Note this is handled + # before the longdistancecycleway-switch, to be able + # to really exlude them be setting cost to infinity + # + switch highway=steps switch allow_steps 40 100000 + switch highway=ferry switch allow_ferries 5.67 100000 + + # + # handle long-distance cycle-routes. + # + switch is_ldcr 1 # always treated as perfect (=1) + add switch stick_to_cycleroutes 0.5 0.05 # everything else somewhat up + + # + # some other highway types + # + switch highway=pedestrian 3 + switch highway=bridleway 5 + switch highway=cycleway 1 + switch or highway=residential highway=living_street switch isunpaved 1.5 1.1 + switch highway=service switch isunpaved 1.6 1.3 + + # + # tracks and track-like ways are rated mainly be tracktype/grade + # But note that if no tracktype is given (mainly for road/path/footway) + # it can be o.k. if there's any other hint for quality + # + switch or highway=track or highway=road or highway=path highway=footway + switch tracktype=grade1 switch probablyGood 1.0 1.3 + switch tracktype=grade2 switch probablyGood 1.1 2.0 + switch tracktype=grade3 switch probablyGood 1.5 3.0 + switch tracktype=grade4 switch probablyGood 2.0 5.0 + switch tracktype=grade5 switch probablyGood 3.0 5.0 + switch probablyGood 1.0 5.0 + + # + # When avoiding unsafe ways, avoid highways without a bike hint + # + add switch and avoid_unsafe not isbike 2 0 + + # + # exclude motorways and proposed roads + # + switch or highway=motorway highway=motorway_link 100000 + switch highway=proposed 100000 + + # + # actuals roads are o.k. if we have a bike hint + # + switch or highway=trunk highway=trunk_link switch isbike 1.5 10 + switch or highway=primary highway=primary_link switch isbike 1.2 3 + switch or highway=secondary highway=secondary_link switch isbike 1.1 1.6 + switch or highway=tertiary highway=tertiary_link switch isbike 1.0 1.4 + switch highway=unclassified switch isbike 1.0 1.3 + + # + # default for any other highway type not handled above + # + 2.0 + + +---context:node # following code refers to node tags + +assign defaultaccess + switch access= + 1 # add default barrier restrictions here! + switch or access=private access=no + 0 + 1 + +assign bikeaccess + or or longdistancecycleway=yes lcn=yes + switch bicycle= + switch vehicle= + defaultaccess + switch or vehicle=private vehicle=no + 0 + 1 + switch or bicycle=private or bicycle=no bicycle=dismount + 0 + 1 + +assign footaccess + or bicycle=dismount + switch foot= + defaultaccess + switch or foot=private foot=no + 0 + 1 + +assign initialcost + switch bikeaccess + 0 + switch footaccess + 100 + 1000000 diff --git a/brouter-server/pom.xml b/brouter-server/pom.xml new file mode 100644 index 0000000..ede752c --- /dev/null +++ b/brouter-server/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + org.btools + brouter + 0.98 + ../pom.xml + + brouter-server + jar + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + jar-with-dependencies + + + + true + btools.server.BRouter + + + + + + make-assembly + package + + single + + + + + + + + + + org.btools + brouter-core + ${project.version} + + + org.btools + brouter-map-creator + ${project.version} + + + diff --git a/brouter-server/src/main/java/btools/server/BRouter.java b/brouter-server/src/main/java/btools/server/BRouter.java new file mode 100644 index 0000000..7bd3dfc --- /dev/null +++ b/brouter-server/src/main/java/btools/server/BRouter.java @@ -0,0 +1,132 @@ +package btools.server; + +import java.io.File; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; + +import btools.router.OsmNodeNamed; +import btools.router.RoutingEngine; +import btools.router.RoutingContext; +import btools.router.OsmTrack; + +public class BRouter +{ + public static void main(String[] args) throws Exception + { + if ( args.length == 2) // cgi-input-mode + { + try + { + String queryString = args[1]; + int sepIdx = queryString.indexOf( '=' ); + if ( sepIdx >= 0 ) queryString = queryString.substring( sepIdx + 1 ); + queryString = URLDecoder.decode( queryString, "ISO-8859-1" ); + int ntokens = 1; + for( int ic = 0; ic maxKm * 1000 ) + { + System.out.println( "airDistance " + (airDistance/1000) + "km exceeds limit for online router (" + maxKm + "km)" ); + return; + } + } + + long maxRunningTime = 60000; // the cgi gets a 1 Minute timeout + String sMaxRunningTime = System.getProperty( "maxRunningTime" ); + if ( sMaxRunningTime != null ) + { + maxRunningTime = Integer.parseInt( sMaxRunningTime ) * 1000; + } + + long startTime = System.currentTimeMillis(); + List wplist = new ArrayList(); + wplist.add( from ); + wplist.add( to ); + + RoutingEngine re = new RoutingEngine( null, null, args[0], wplist, readRoutingContext(a2) ); + re.doRun( maxRunningTime ); + if ( re.getErrorMessage() != null ) + { + System.out.println( re.getErrorMessage() ); + } + } + catch( Throwable e ) + { + System.out.println( "unexpected exception: " + e ); + } + System.exit(0); + } + System.out.println("BRouter 0.98 / 12012014 / abrensch"); + if ( args.length < 6 ) + { + System.out.println("Find routes in an OSM map"); + System.out.println("usage: java -jar brouter.jar "); + return; + } + List wplist = new ArrayList(); + wplist.add( readPosition( args, 1, "from" ) ); + wplist.add( readPosition( args, 3, "to" ) ); + RoutingEngine re = new RoutingEngine( "mytrack", "mylog", args[0], wplist, readRoutingContext(args) ); + re.doRun( 0 ); + if ( re.getErrorMessage() != null ) + { + System.out.println( re.getErrorMessage() ); + } + } + + + private static OsmNodeNamed readPosition( String[] args, int idx, String name ) + { + OsmNodeNamed n = new OsmNodeNamed(); + n.name = name; + n.ilon = (int)( ( Double.parseDouble( args[idx ] ) + 180. ) *1000000. + 0.5); + n.ilat = (int)( ( Double.parseDouble( args[idx+1] ) + 90. ) *1000000. + 0.5); + return n; + } + + private static RoutingContext readRoutingContext( String[] args ) + { + RoutingContext c = new RoutingContext(); + if ( args.length > 5 ) + { + c.localFunction = args[5]; + if ( args.length > 6 ) + { + c.setAlternativeIdx( Integer.parseInt( args[6] ) ); + } + } + return c; + } +} diff --git a/brouter-server/src/main/java/btools/server/CgiUpload.java b/brouter-server/src/main/java/btools/server/CgiUpload.java new file mode 100644 index 0000000..d71c77f --- /dev/null +++ b/brouter-server/src/main/java/btools/server/CgiUpload.java @@ -0,0 +1,111 @@ +package btools.server; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.URLDecoder; + +public class CgiUpload +{ + public static void main(String[] args) + { + try + { + _main(args); + } + catch( Exception e ) + { + System.out.println( "unexpected exception: " + e ); + } + } + + private static void _main(String[] args) throws Exception + { + String htmlTemplate = args[0]; + String customeProfileDir = args[1]; + + String id = "" + System.currentTimeMillis(); + + + // cgi-header + System.out.println( "Content-type: text/html" ); + System.out.println(); + + // write the post message to a file + BufferedWriter bw = new BufferedWriter( + new OutputStreamWriter( + new FileOutputStream( customeProfileDir + "/" + id + ".brf" ) ) ); + BufferedReader ir = new BufferedReader( new InputStreamReader( System.in ) ); + String postData = ir.readLine(); + String[] coordValues = new String[4]; + if( postData != null ) + { + int coordsIdx = postData.indexOf( "coords=" ); + if ( coordsIdx >= 0) + { + int coordsEnd = postData.indexOf( '&' ); + if ( coordsEnd >= 0) + { + String coordsString = postData.substring( coordsIdx + 7, coordsEnd ); + postData = postData.substring( coordsEnd+1 ); + int pos = 0; + for(int idx=0; idx<4; idx++) + { + int p = coordsString.indexOf( '_', pos ); + coordValues[idx] = coordsString.substring( pos, p ); + pos = p+1; + } + } + } + int sepIdx = postData.indexOf( '=' ); + if ( sepIdx >= 0 ) postData = postData.substring( sepIdx + 1 ); + postData = URLDecoder.decode( postData, "ISO-8859-1" ); + bw.write( postData ); + } + bw.close(); + + // echo the template with a custom select item + BufferedReader br = new BufferedReader( + new InputStreamReader( + new FileInputStream( htmlTemplate ) ) ); + + for(;;) + { + String line = br.readLine(); + if ( line == null ) break; + if ( line.indexOf( "" ) >= 0 ) + { + line = " "; + } + else if ( line.indexOf( "paste your profile here" ) >= 0 ) + { + System.out.println( ""; + } + else + { + line = replaceCoord( line, "lonfrom", coordValues[0] ); + line = replaceCoord( line, "latfrom", coordValues[1] ); + line = replaceCoord( line, "lonto", coordValues[2] ); + line = replaceCoord( line, "latto", coordValues[3] ); + } + + System.out.println( line ); + } + br.close(); + } + + private static String replaceCoord( String line, String name, String value ) + { + String inputTag = "= 0 ) + { + return inputTag + " value=\"" + value + "\">"; + } + return line; + } +} diff --git a/brouter-server/src/main/java/btools/server/RouteServer.java b/brouter-server/src/main/java/btools/server/RouteServer.java new file mode 100644 index 0000000..195b62c --- /dev/null +++ b/brouter-server/src/main/java/btools/server/RouteServer.java @@ -0,0 +1,267 @@ +package btools.server; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import btools.router.OsmNodeNamed; +import btools.router.OsmTrack; +import btools.router.RoutingContext; +import btools.router.RoutingEngine; +import btools.server.request.RequestHandler; +import btools.server.request.ServerHandler; +import btools.server.request.YoursHandler; + +public class RouteServer extends Thread +{ + public ServiceContext serviceContext; + public short port = 17777; + + private boolean serverStopped = false; + private ServerSocket serverSocket = null; + + public void close() + { + serverStopped = true; + try + { + ServerSocket ss = serverSocket; + serverSocket = null; + ss.close(); + } + catch( Throwable t ) {} + } + + private void killOtherServer() throws Exception + { + Socket socket = new Socket( "localhost", port ); + BufferedWriter bw = null; + try + { + bw = new BufferedWriter( new OutputStreamWriter( socket.getOutputStream() ) ); + bw.write( "EXIT\n" ); + } + finally + { + bw.close(); + socket.close(); + } + } + + public void run() + { + // first go an kill any other server on that port + + for(;;) + { + try + { + killOtherServer(); + System.out.println( "killed, waiting" ); + try { Thread.sleep( 3000 ); } catch( InterruptedException ie ) {} + } + catch( Throwable t ) { + System.out.println( "not killed: " + t ); + break; + } + } + try + { + serverSocket = new ServerSocket(port); + for(;;) + { + System.out.println("RouteServer accepting connections.."); + Socket clientSocket = serverSocket.accept(); + if ( !serveRequest( clientSocket ) ) break; + } + } + catch( Throwable e ) + { + System.out.println("RouteServer main loop got exception (exiting): "+e); + if ( serverSocket != null ) + { + try { serverSocket.close(); } catch( Throwable t ) {} + } + System.exit(0); + } + + } + + + + public boolean serveRequest( Socket clientSocket ) + { + BufferedReader br = null; + BufferedWriter bw = null; + try + { + br = new BufferedReader( new InputStreamReader( clientSocket.getInputStream() ) ); + bw = new BufferedWriter( new OutputStreamWriter( clientSocket.getOutputStream() ) ); + + // we just read the first line + String getline = br.readLine(); + if ( getline == null || getline.startsWith( "EXIT") ) + { + throw new RuntimeException( "socketExitRequest" ); + } + if ( getline.startsWith("GET /favicon.ico") ) + { + return true; + } + + String url = getline.split(" ")[1]; + HashMap params = getUrlParams(url); + + long maxRunningTime = getMaxRunningTime(); + long startTime = System.currentTimeMillis(); + + RequestHandler handler; + if ( params.containsKey( "lonlats" ) && params.containsKey( "profile" ) ) + { + handler = new ServerHandler( serviceContext, params ); + } + else + { + handler = new YoursHandler( serviceContext, params ); + } + RoutingContext rc = handler.readRoutingContext(); + List wplist = handler.readWayPointList(); + + RoutingEngine cr = new RoutingEngine( null, null, serviceContext.segmentDir, wplist, rc ); + cr.quite = true; + cr.doRun( maxRunningTime ); + + // http-header + bw.write( "HTTP/1.1 200 OK\n" ); + bw.write( "Connection: close\n" ); + bw.write( "Content-Type: text/xml; charset=utf-8\n" ); + bw.write( "Access-Control-Allow-Origin: *\n" ); + bw.write( "\n" ); + + if ( cr.getErrorMessage() != null ) + { + bw.write( cr.getErrorMessage() ); + bw.write( "\n" ); + } + else + { + OsmTrack track = cr.getFoundTrack(); + if ( track != null ) + { + bw.write( handler.formatTrack(track) ); + } + } + bw.flush(); + } + catch (Throwable e) + { + if ( "socketExitRequest".equals( e.getMessage() ) ) + { + return false; + } + System.out.println("RouteServer got exception (will continue): "+e); + e.printStackTrace(); + } + finally + { + if ( br != null ) try { br.close(); } catch( Exception e ) {} + if ( bw != null ) try { bw.close(); } catch( Exception e ) {} + } + return true; + } + + public static void main(String[] args) throws Exception + { + System.out.println("BRouter 0.98 / 12012014 / abrensch"); + if ( args.length != 3 ) + { + System.out.println("serve YOURS protocol for BRouter"); + System.out.println("usage: java RouteServer "); + System.out.println(""); + System.out.println("serve BRouter protocol"); + System.out.println("usage: java RouteServer "); + return; + } + + ServiceContext serviceContext = new ServiceContext(); + serviceContext.segmentDir = args[0]; + File profileMapOrDir = new File( args[1] ); + if ( profileMapOrDir.isDirectory() ) + { + System.setProperty( "profileBaseDir", args[1] ); + } + else + { + serviceContext.profileMap = loadProfileMap( profileMapOrDir ); + } + + ServerSocket serverSocket = new ServerSocket(Integer.parseInt(args[2])); + for (;;) + { + Socket clientSocket = serverSocket.accept(); + RouteServer server = new RouteServer(); + server.serviceContext = serviceContext; + server.serveRequest( clientSocket ); + } + } + + private static Map loadProfileMap( File file ) throws IOException + { + Map profileMap = new HashMap(); + + BufferedReader pr = new BufferedReader( new InputStreamReader( new FileInputStream( file ) ) ); + for(;;) + { + String key = pr.readLine(); + if ( key == null ) break; + key = key.trim(); + if ( key.length() == 0 ) continue; + String value = pr.readLine(); + value = value.trim(); + profileMap.put( key, value ); + } + + return profileMap; + } + + private static HashMap getUrlParams( String url ) + { + HashMap params = new HashMap(); + StringTokenizer tk = new StringTokenizer( url, "?&" ); + while( tk.hasMoreTokens() ) + { + String t = tk.nextToken(); + StringTokenizer tk2 = new StringTokenizer( t, "=" ); + if ( tk2.hasMoreTokens() ) + { + String key = tk2.nextToken(); + if ( tk2.hasMoreTokens() ) + { + String value = tk2.nextToken(); + params.put( key, value ); + } + } + } + return params; + } + + private static long getMaxRunningTime() { + long maxRunningTime = 60000; + String sMaxRunningTime = System.getProperty( "maxRunningTime" ); + if ( sMaxRunningTime != null ) + { + maxRunningTime = Integer.parseInt( sMaxRunningTime ) * 1000; + } + return maxRunningTime; + } +} diff --git a/brouter-server/src/main/java/btools/server/ServiceContext.java b/brouter-server/src/main/java/btools/server/ServiceContext.java new file mode 100644 index 0000000..096a78d --- /dev/null +++ b/brouter-server/src/main/java/btools/server/ServiceContext.java @@ -0,0 +1,16 @@ +package btools.server; + +import java.util.List; +import java.util.Map; + +import btools.router.OsmNodeNamed; + +/** + * Environment configuration that is initialized at server/service startup + */ +public class ServiceContext +{ + public String segmentDir; + public Map profileMap = null; + public List nogoList; +} diff --git a/brouter-server/src/main/java/btools/server/request/RequestHandler.java b/brouter-server/src/main/java/btools/server/request/RequestHandler.java new file mode 100644 index 0000000..060603b --- /dev/null +++ b/brouter-server/src/main/java/btools/server/request/RequestHandler.java @@ -0,0 +1,28 @@ +package btools.server.request; + +import java.util.HashMap; +import java.util.List; + +import btools.router.OsmNodeNamed; +import btools.router.OsmTrack; +import btools.router.RoutingContext; +import btools.server.ServiceContext; + +public abstract class RequestHandler +{ + protected ServiceContext serviceContext; + protected HashMap params; + + public RequestHandler( ServiceContext serviceContext, HashMap params ) + { + this.serviceContext = serviceContext; + this.params = params; + } + + public abstract RoutingContext readRoutingContext(); + + public abstract List readWayPointList(); + + public abstract String formatTrack(OsmTrack track); + +} \ No newline at end of file diff --git a/brouter-server/src/main/java/btools/server/request/ServerHandler.java b/brouter-server/src/main/java/btools/server/request/ServerHandler.java new file mode 100644 index 0000000..2ca962a --- /dev/null +++ b/brouter-server/src/main/java/btools/server/request/ServerHandler.java @@ -0,0 +1,149 @@ +package btools.server.request; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import btools.router.OsmNodeNamed; +import btools.router.OsmTrack; +import btools.router.RoutingContext; +import btools.server.ServiceContext; + +/** + * URL query parameter handler for web and standalone server. Supports all + * BRouter features without restrictions. + * + * Parameters: + * + * lonlats = lon,lat|... (unlimited list of lon,lat waypoints separated by |) + * nogos = lon,lat,radius|... (optional, radius in meters) + * profile = profile file name without .brf + * alternativeidx = [0|1|2|3] (optional, default 0) + * format = [kml|gpx] (optional, default gpx) + * + * Example URLs: + * http://localhost:17777/brouter?lonlats=8.799297,49.565883|8.811764,49.563606&nogos=&profile=trekking&alternativeidx=0&format=gpx + * http://localhost:17777/brouter?lonlats=1.1,1.2|2.1,2.2|3.1,3.2|4.1,4.2&nogos=-1.1,-1.2,1|-2.1,-2.2,2&profile=shortest&alternativeidx=1&format=kml + * + */ +public class ServerHandler extends RequestHandler { + + public ServerHandler( ServiceContext serviceContext, HashMap params ) + { + super( serviceContext, params ); + } + + @Override + public RoutingContext readRoutingContext() + { + RoutingContext rc = new RoutingContext(); + + rc.localFunction = params.get( "profile" ); + rc.setAlternativeIdx(Integer.parseInt(params.get( "alternativeidx" ))); + + List nogoList = readNogoList(); + if ( nogoList != null ) + { + rc.prepareNogoPoints( nogoList ); + rc.nogopoints = nogoList; + } + + return rc; + } + + @Override + public List readWayPointList() + { + // lon,lat|... + String lonLats = params.get( "lonlats" ); + if (lonLats == null) throw new IllegalArgumentException( "lonlats parameter not set" ); + + String[] coords = lonLats.split("\\|"); + if (coords.length < 2) throw new IllegalArgumentException( "we need two lat/lon points at least!" ); + + List wplist = new ArrayList(); + for (int i = 0; i < coords.length; i++) + { + String[] lonLat = coords[i].split(","); + wplist.add( readPosition( lonLat[0], lonLat[1], "via" + i ) ); + } + + wplist.get(0).name = "from"; + wplist.get(wplist.size()-1).name = "to"; + + return wplist; + } + + @Override + public String formatTrack(OsmTrack track) + { + String result; + // optional, may be null + String format = params.get( "format" ); + + if (format == null || "gpx".equals(format)) + { + result = track.formatAsGpx(); + } + else if ("kml".equals(format)) + { + result = track.formatAsKml(); + } + else { + System.out.println("unknown track format '" + format + "', using default"); + result = track.formatAsGpx(); + } + + return result; + } + + private static OsmNodeNamed readPosition( String vlon, String vlat, String name ) + { + if ( vlon == null ) throw new IllegalArgumentException( "lon " + name + " not found in input" ); + if ( vlat == null ) throw new IllegalArgumentException( "lat " + name + " not found in input" ); + + return readPosition(Double.parseDouble( vlon ), Double.parseDouble( vlat ), name); + } + + private static OsmNodeNamed readPosition( double lon, double lat, String name ) + { + OsmNodeNamed n = new OsmNodeNamed(); + n.name = name; + n.ilon = (int)( ( lon + 180. ) *1000000. + 0.5); + n.ilat = (int)( ( lat + 90. ) *1000000. + 0.5); + return n; + } + + private List readNogoList() + { + // lon,lat,radius|... + String nogos = params.get( "nogos" ); + if ( nogos == null ) return null; + + String[] lonLatRadList = nogos.split("\\|"); + + List nogoList = new ArrayList(); + for (int i = 0; i < lonLatRadList.length; i++) + { + String[] lonLatRad = lonLatRadList[i].split(","); + nogoList.add(readNogo(lonLatRad[0], lonLatRad[1], lonLatRad[2])); + } + + return nogoList; + } + + private static OsmNodeNamed readNogo( String lon, String lat, String radius ) + { + return readNogo(Double.parseDouble( lon ), Double.parseDouble( lat ), Integer.parseInt( radius ) ); + } + + private static OsmNodeNamed readNogo( double lon, double lat, int radius ) + { + OsmNodeNamed n = new OsmNodeNamed(); + n.name = "nogo" + radius; + n.ilon = (int)( ( lon + 180. ) *1000000. + 0.5); + n.ilat = (int)( ( lat + 90. ) *1000000. + 0.5); + n.isNogo = true; + return n; + } +} diff --git a/brouter-server/src/main/java/btools/server/request/YoursHandler.java b/brouter-server/src/main/java/btools/server/request/YoursHandler.java new file mode 100644 index 0000000..4c28634 --- /dev/null +++ b/brouter-server/src/main/java/btools/server/request/YoursHandler.java @@ -0,0 +1,69 @@ +package btools.server.request; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import btools.router.OsmNodeNamed; +import btools.router.OsmTrack; +import btools.router.RoutingContext; +import btools.server.ServiceContext; + +public class YoursHandler extends RequestHandler { + + public YoursHandler( ServiceContext serviceContext, HashMap params ) + { + super(serviceContext, params); + } + + @Override + public RoutingContext readRoutingContext() + { + RoutingContext rc = new RoutingContext(); + + String profile_key = params.get( "v" ) + " " + params.get( "fast" ); + if (serviceContext.profileMap == null) throw new IllegalArgumentException( "no profile map loaded" ); + String profile_path = serviceContext.profileMap.get( profile_key ); + if ( profile_path == null ) profile_path = serviceContext.profileMap.get( "default" ); + if ( profile_path == null ) throw new IllegalArgumentException( "no profile for key: " + profile_key ); + rc.localFunction = profile_path; + + List nogoList = serviceContext.nogoList; + if ( nogoList != null ) + { + rc.prepareNogoPoints( nogoList ); + rc.nogopoints = nogoList; + } + + return rc; + } + + @Override + public List readWayPointList() + { + List wplist = new ArrayList(); + wplist.add( readPosition( params, "flon", "flat", "from" ) ); + wplist.add( readPosition( params, "tlon", "tlat", "to" ) ); + return wplist; + } + + @Override + public String formatTrack(OsmTrack track) + { + return track.formatAsKml(); + } + + private static OsmNodeNamed readPosition( HashMap params, String plon, String plat, String name ) + { + String vlon = params.get( plon ); + if ( vlon == null ) throw new IllegalArgumentException( "param " + plon + " bot found in input" ); + String vlat = params.get( plat ); + if ( vlat == null ) throw new IllegalArgumentException( "param " + plat + " bot found in input" ); + + OsmNodeNamed n = new OsmNodeNamed(); + n.name = name; + n.ilon = (int)( ( Double.parseDouble( vlon ) + 180. ) *1000000. + 0.5); + n.ilat = (int)( ( Double.parseDouble( vlat ) + 90. ) *1000000. + 0.5); + return n; + } +} diff --git a/brouter-util/pom.xml b/brouter-util/pom.xml new file mode 100644 index 0000000..4bad419 --- /dev/null +++ b/brouter-util/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + org.btools + brouter + 0.98 + ../pom.xml + + brouter-util + jar + + + + junit + junit + + + + diff --git a/brouter-util/src/main/java/btools/util/CompactLongMap.java b/brouter-util/src/main/java/btools/util/CompactLongMap.java new file mode 100644 index 0000000..314772b --- /dev/null +++ b/brouter-util/src/main/java/btools/util/CompactLongMap.java @@ -0,0 +1,328 @@ +package btools.util; + +import java.util.ArrayList; + +/** + * Memory efficient Map to map a long-key to an object-value + * + * Implementation is such that basically the 12 bytes + * per entry is allocated that's needed to store + * a long- and an object-value. + * This class does not implement the Map interface + * because it's not complete (remove() is not implemented, + * CompactLongMap can only grow.) + * + * @author ab + */ +public class CompactLongMap +{ + private long[][] al; + private int[] pa; + private int size = 0; + private int _maxKeepExponent = 14; // the maximum exponent to keep the invalid arrays + + private V value_in; + protected V value_out; + + protected static final int MAXLISTS = 31; // enough for size Integer.MAX_VALUE + private static boolean earlyDuplicateCheck; + + public CompactLongMap() + { + // pointer array + pa = new int[MAXLISTS]; + + // allocate key lists + al = new long[MAXLISTS][]; + al[0] = new long[1]; // make the first array (the transient buffer) + + // same for the values + vla = new Object[MAXLISTS][]; + vla[0] = new Object[1]; + + earlyDuplicateCheck = Boolean.getBoolean( "earlyDuplicateCheck" ); + } + + + /* + * + * The Map extension: + * next 5 protected methods are needed to implement value-support + * overwrite them all to support value structures other than the + * long-values implemented here as a sample. + * + * Furthermore, put() and get() method need to be implemented + * to access the values. + * + * Note that this map does not behave exactly like java.util.Map + * - put(..) with already existing key throws exception + * - get(..) with non-existing key thros exception + * + * If you have keys that cannot easily be mapped on long's, use + * a hash-function to do the mapping. But note that, in comparison + * to java.util.HashMap, in that case the keys itself are not saved, + * only the hash-values, so you need to be sure that random duplicate + * hashs are either excluded by the structure of your data or that + * you can handle the possible IllegalArgumentException + * + */ + + private Object[][] vla; // value list array + + + public boolean put( long id, V value ) + { + try + { + value_in = value; + if ( contains( id, true ) ) + { + return true; + } + vla[0][0] = value; + _add( id ); + return false; + } + finally + { + value_in = null; + value_out = null; + } + } + + /** + * Same as put( id, value ) but duplicate check + * is skipped for performance. Be aware that you + * can get a duplicate exception later on if the + * map is restructured! + * with System parameter earlyDuplicateCheck=true you + * can enforce the early duplicate check for debugging + * + * @param id the key to insert + * @param value the value to insert object + * @exception IllegalArgumentException for duplicates if enabled + */ + public void fastPut( long id, V value ) + { + if ( earlyDuplicateCheck && contains( id ) ) + { + throw new IllegalArgumentException( "duplicate key found in early check: " + id ); + } + vla[0][0] = value; + _add( id ); + } + + /** + * Get the value for the given id + * @param id the key to query + * @return the object, or null if id not known + */ + public V get( long id ) + { + try + { + if ( contains( id, false ) ) + { + return value_out; + } + return null; + } + finally + { + value_out = null; + } + } + + + /** + * @return the number of entries in this map + */ + public int size() + { + return size; + } + + + private boolean _add( long id ) + { + if ( size == Integer.MAX_VALUE ) + { + throw new IllegalArgumentException( "cannot grow beyond size Integer.MAX_VALUE" ); + } + + // put the new entry in the first array + al[0][0] = id; + + // determine the first empty array + int bp = size++; // treat size as bitpattern + int idx = 1; + int n = 1; + + pa[0] = 1; + pa[1] = 1; + + while ( (bp&1) == 1 ) + { + bp >>= 1; + pa[idx++] = n; + n <<= 1; + } + + // create it if not existant + if ( al[idx] == null ) + { + al[idx] = new long[n]; + vla[idx] = new Object[n]; + } + + // now merge the contents of arrays 0...idx-1 into idx + while ( n > 0 ) + { + long maxId = 0; + int maxIdx = -1; + + for ( int i=0; i 0 ) + { + long currentId = al[i][p-1]; + if ( maxIdx < 0 || currentId > maxId ) + { + maxIdx = i; + maxId = currentId; + } + } + } + + // current maximum found, copy to target array + if ( n < al[idx].length && maxId == al[idx][n] ) + { + throw new IllegalArgumentException( "duplicate key found in late check: " + maxId ); + } + --n; + al[idx][n] = maxId; + vla[idx][n] = vla[maxIdx][pa[maxIdx]-1]; + + --pa[maxIdx]; + } + + // de-allocate empty arrays of a certain size (fix at 64kByte) + while ( idx-- > _maxKeepExponent ) + { + al[idx] = null; + vla[idx] = null; + } + + return false; + } + + /** + * @return true if "id" is contained in this set. + */ + public boolean contains( long id ) + { + try + { + return contains( id, false ); + } + finally + { + value_out = null; + } + } + + protected boolean contains( long id, boolean doPut ) + { + // determine the first empty array + int bp = size; // treat size as bitpattern + int idx = 1; + + while ( bp != 0 ) + { + if ( (bp&1) == 1 ) + { + // array at idx is valid, check + if ( contains( idx, id, doPut ) ) + { + return true; + } + } + idx++; + bp >>= 1; + } + return false; + } + + + // does sorted array "a" contain "id" ? + private boolean contains( int idx, long id, boolean doPut ) + { + long[] a = al[idx]; + int offset = a.length; + int n = 0; + + while ( (offset >>= 1) > 0 ) + { + int nn = n + offset; + if ( a[nn] <= id ) + { + n = nn; + } + } + if ( a[n] == id ) + { + value_out = (V)vla[idx][n]; + if ( doPut ) vla[idx][n] = value_in; + return true; + } + return false; + } + + protected void moveToFrozenArrays( long[] faid, ArrayList flv ) + { + for( int i=1; i>= 1; + } + faid[ti] = minId; + flv.add( (V)vla[minIdx][pa[minIdx]] ); + pa[minIdx]++; + + if ( ti > 0 && faid[ti-1] == minId ) + { + throw new IllegalArgumentException( "duplicate key found in late check: " + minId ); + } + } + + // free the non-frozen arrays + al = null; + vla = null; + } + +} diff --git a/brouter-util/src/main/java/btools/util/CompactLongSet.java b/brouter-util/src/main/java/btools/util/CompactLongSet.java new file mode 100644 index 0000000..08dace4 --- /dev/null +++ b/brouter-util/src/main/java/btools/util/CompactLongSet.java @@ -0,0 +1,220 @@ +package btools.util; + +/** + * Memory efficient Set for long-keys + * + * @author ab + */ +public class CompactLongSet +{ + private long[][] al; + private int[] pa; + private int size = 0; + private int _maxKeepExponent = 14; // the maximum exponent to keep the invalid arrays + + protected static final int MAXLISTS = 31; // enough for size Integer.MAX_VALUE + private static boolean earlyDuplicateCheck; + + public CompactLongSet() + { + // pointer array + pa = new int[MAXLISTS]; + + // allocate key lists + al = new long[MAXLISTS][]; + al[0] = new long[1]; // make the first array (the transient buffer) + + earlyDuplicateCheck = Boolean.getBoolean( "earlyDuplicateCheck" ); + } + + + /** + * @return the number of entries in this set + */ + public int size() + { + return size; + } + + /** + * add a long value to this set if not yet in. + * @param id the value to add to this set. + * @return true if "id" already contained in this set. + */ + public boolean add( long id ) + { + if ( contains( id ) ) + { + return true; + } + _add( id ); + return false; + } + + public void fastAdd( long id ) + { + _add( id ); + } + + private void _add( long id ) + { + if ( size == Integer.MAX_VALUE ) + { + throw new IllegalArgumentException( "cannot grow beyond size Integer.MAX_VALUE" ); + } + + // put the new entry in the first array + al[0][0] = id; + + // determine the first empty array + int bp = size++; // treat size as bitpattern + int idx = 1; + int n = 1; + + pa[0] = 1; + pa[1] = 1; + + while ( (bp&1) == 1 ) + { + bp >>= 1; + pa[idx++] = n; + n <<= 1; + } + + // create it if not existant + if ( al[idx] == null ) + { + al[idx] = new long[n]; + } + + // now merge the contents of arrays 0...idx-1 into idx + while ( n > 0 ) + { + long maxId = 0; + int maxIdx = -1; + + for ( int i=0; i 0 ) + { + long currentId = al[i][p-1]; + if ( maxIdx < 0 || currentId > maxId ) + { + maxIdx = i; + maxId = currentId; + } + } + } + + // current maximum found, copy to target array + if ( n < al[idx].length && maxId == al[idx][n] ) + { + throw new IllegalArgumentException( "duplicate key found in late check: " + maxId ); + } + --n; + al[idx][n] = maxId; + + --pa[maxIdx]; + } + + // de-allocate empty arrays of a certain size (fix at 64kByte) + while ( idx-- > _maxKeepExponent ) + { + al[idx] = null; + } + } + + /** + * @return true if "id" is contained in this set. + */ + public boolean contains( long id ) + { + // determine the first empty array + int bp = size; // treat size as bitpattern + int idx = 1; + + while ( bp != 0 ) + { + if ( (bp&1) == 1 ) + { + // array at idx is valid, check + if ( contains( idx, id ) ) + { + return true; + } + } + idx++; + bp >>= 1; + } + return false; + } + + + // does sorted array "a" contain "id" ? + private boolean contains( int idx, long id ) + { + long[] a = al[idx]; + int offset = a.length; + int n = 0; + + while ( (offset >>= 1) > 0 ) + { + int nn = n + offset; + if ( a[nn] <= id ) + { + n = nn; + } + } + if ( a[n] == id ) + { + return true; + } + return false; + } + + protected void moveToFrozenArray( long[] faid ) + { + for( int i=1; i>= 1; + } + faid[ti] = minId; + pa[minIdx]++; + + if ( ti > 0 && faid[ti-1] == minId ) + { + throw new IllegalArgumentException( "duplicate key found in late check: " + minId ); + } + } + + // free the non-frozen array + al = null; + } + +} diff --git a/brouter-util/src/main/java/btools/util/DenseLongMap.java b/brouter-util/src/main/java/btools/util/DenseLongMap.java new file mode 100644 index 0000000..00b7dcc --- /dev/null +++ b/brouter-util/src/main/java/btools/util/DenseLongMap.java @@ -0,0 +1,140 @@ +package btools.util; + +import java.util.ArrayList; + +/** + * Special Memory efficient Map to map a long-key to + * a "small" value (some bits only) where it is expected + * that the keys are dense, so that we can use more or less + * a simple array as the best-fit data model (except for + * the 32-bit limit of arrays!) + * + * Additionally, to enable small-memory unit testing + * of code using this map this one has a fallback for a small map + * where we have only few keys, but not dense. In this + * case, we use the mechanics of the CompactLongMap + * + * Target application are osm-node ids which are in the + * range 0...3 billion and basically dense (=only few + * nodes deleted) + * + * @author ab + */ +public class DenseLongMap +{ + private ArrayList blocklist = new ArrayList(1024); + + private static final int BLOCKSIZE = 0x10000; // 64k * 32 bits + private int valuebits; + private int maxvalue; + private long maxkey; + private long maxmemory; + + /** + * Creates a DenseLongMap for the given value range + * Note that one value is reserved for the "unset" state, + * so with 6 value bits you can store values in the + * range 0..62 only + * + * @param valuebits number of bits to use per value + */ + public DenseLongMap( int valuebits ) + { + if ( valuebits < 1 || valuebits > 32 ) + { + throw new IllegalArgumentException( "invalid valuebits (1..32): " + valuebits ); + } + this.valuebits = valuebits; + maxmemory = (Runtime.getRuntime().maxMemory() / 8) * 7; // assume most of it for our map + maxvalue = (1 << valuebits) - 2; + maxkey = ( maxmemory / valuebits ) * 8; + } + + + + public void put( long key, int value ) + { + if ( key < 0L || key > maxkey ) + { + throw new IllegalArgumentException( "key out of range (0.." + maxkey + "): " + key + + " give more memory (currently " + (maxmemory / 0x100000) + + "MB) to extend key range" ); + } + if ( value < 0 || value > maxvalue ) + { + throw new IllegalArgumentException( "value out of range (0.." + maxvalue + "): " + value ); + } + + int blockn = (int)(key >> 21); + int offset = (int)(key & 0x1fffff); + + int[] block = blockn < blocklist.size() ? blocklist.get( blockn ) : null; + + if ( block == null ) + { + block = new int[BLOCKSIZE * valuebits]; + + while (blocklist.size() < blockn+1 ) + { + blocklist.add(null); + } + blocklist.set( blockn, block ); + } + + int bitmask = 1 << (offset & 0x1f); + int invmask = bitmask ^ 0xffffffff; + int probebit = 1; + int blockidx = (offset >> 5)*valuebits; + int blockend = blockidx + valuebits; + int v = value + 1; // 0 is reserved (=unset) + + while( blockidx < blockend ) + { + if ( ( v & probebit ) != 0 ) + { + block[blockidx] |= bitmask; + } + else + { + block[blockidx] &= invmask; + } + probebit <<= 1; + blockidx++; + } + } + + + public int getInt( long key ) + { + if ( key < 0 ) + { + return -1; + } + int blockn = (int)(key >> 21); + int offset = (int)(key & 0x1fffff); + + int[] block = blockn < blocklist.size() ? blocklist.get( blockn ) : null; + + if ( block == null ) + { + return -1; + } + int bitmask = 1 << (offset & 0x1f); + int probebit = 1; + int blockidx = (offset >> 5)*valuebits; + int blockend = blockidx + valuebits; + int v = 0; // 0 is reserved (=unset) + + while( blockidx < blockend ) + { + if ( ( block[blockidx] & bitmask ) != 0 ) + { + v |= probebit; + } + probebit <<= 1; + blockidx++; + } + return v-1; + } + +} diff --git a/brouter-util/src/main/java/btools/util/FrozenLongMap.java b/brouter-util/src/main/java/btools/util/FrozenLongMap.java new file mode 100644 index 0000000..b308173 --- /dev/null +++ b/brouter-util/src/main/java/btools/util/FrozenLongMap.java @@ -0,0 +1,122 @@ +package btools.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * Frozen instance of Memory efficient Map + * + * This one is readily sorted into a singe array for faster access + * + * @author ab + */ +public class FrozenLongMap extends CompactLongMap +{ + private long[] faid; + private ArrayList flv; + private int size = 0; + private int p2size; // next power of 2 of size + + public FrozenLongMap( CompactLongMap map ) + { + size = map.size(); + + faid = new long[size]; + flv = new ArrayList(size); + + map.moveToFrozenArrays( faid, flv ); + + p2size = 0x40000000; + while( p2size > size ) p2size >>= 1; + } + + @Override + public boolean put( long id, V value ) + { + throw new RuntimeException( "cannot put on FrozenLongIntMap" ); + } + + @Override + public void fastPut( long id, V value ) + { + throw new RuntimeException( "cannot put on FrozenLongIntMap" ); + } + + /** + * @return the number of entries in this set + */ + @Override + public int size() + { + return size; + } + + + + /** + * @return true if "id" is contained in this set. + */ + @Override + protected boolean contains( long id, boolean doPut ) + { + if ( size == 0 ) + { + return false; + } + long[] a = faid; + int offset = p2size; + int n = 0; + + while ( offset> 0 ) + { + int nn = n + offset; + if ( nn < size && a[nn] <= id ) + { + n = nn; + } + offset >>= 1; + } + if ( a[n] == id ) + { + value_out = flv.get(n); + return true; + } + return false; + } + + /** + * @return the value for "id", + * Throw an exception if not contained in the map. + */ + @Override + public V get( long id ) + { + if ( size == 0 ) + { + return null; + } + long[] a = faid; + int offset = p2size; + int n = 0; + + while ( offset> 0 ) + { + int nn = n + offset; + if ( nn < size && a[nn] <= id ) + { + n = nn; + } + offset >>= 1; + } + if ( a[n] == id ) + { + return flv.get(n); + } + return null; + } + + public List getValueList() + { + return flv; + } +} diff --git a/brouter-util/src/main/java/btools/util/FrozenLongSet.java b/brouter-util/src/main/java/btools/util/FrozenLongSet.java new file mode 100644 index 0000000..f6ef6b3 --- /dev/null +++ b/brouter-util/src/main/java/btools/util/FrozenLongSet.java @@ -0,0 +1,81 @@ +package btools.util; + +/** + * Frozen instance of Memory efficient Set + * + * This one is readily sorted into a singe array for faster access + * + * @author ab + */ +public class FrozenLongSet extends CompactLongSet +{ + private long[] faid; + private int size = 0; + private int p2size; // next power of 2 of size + + public FrozenLongSet( CompactLongSet set ) + { + size = set.size(); + + faid = new long[size]; + + set.moveToFrozenArray( faid ); + + p2size = 0x40000000; + while( p2size > size ) p2size >>= 1; + } + + @Override + public boolean add( long id ) + { + throw new RuntimeException( "cannot add on FrozenLongSet" ); + } + + @Override + public void fastAdd( long id ) + { + throw new RuntimeException( "cannot add on FrozenLongSet" ); + } + + /** + * @return the number of entries in this set + */ + @Override + public int size() + { + return size; + } + + + + /** + * @return true if "id" is contained in this set. + */ + @Override + public boolean contains( long id ) + { + if ( size == 0 ) + { + return false; + } + long[] a = faid; + int offset = p2size; + int n = 0; + + while ( offset> 0 ) + { + int nn = n + offset; + if ( nn < size && a[nn] <= id ) + { + n = nn; + } + offset >>= 1; + } + if ( a[n] == id ) + { + return true; + } + return false; + } + +} diff --git a/brouter-util/src/main/java/btools/util/LazyArrayOfLists.java b/brouter-util/src/main/java/btools/util/LazyArrayOfLists.java new file mode 100644 index 0000000..850504b --- /dev/null +++ b/brouter-util/src/main/java/btools/util/LazyArrayOfLists.java @@ -0,0 +1,53 @@ +package btools.util; + +import java.util.List; +import java.util.ArrayList; + +/** + * Behaves like an Array of list + * with lazy list-allocation at getList + * + * @author ab + */ +public class LazyArrayOfLists +{ + private ArrayList> lists; + + public LazyArrayOfLists( int size ) + { + lists = new ArrayList>( size ); + for ( int i = 0; i< size; i++ ) + { + lists.add( null ); + } + } + + public List getList( int idx ) + { + ArrayList list = lists.get( idx ); + if ( list == null ) + { + list = new ArrayList(); + lists.set( idx, list ); + } + return list; + } + + public int getSize( int idx ) + { + List list = lists.get( idx ); + return list == null ? 0 : list.size(); + } + + public void trimAll() + { + for ( int idx = 0; idx< lists.size(); idx++ ) + { + ArrayList list = lists.get( idx ); + if ( list != null ) + { + list.trimToSize(); + } + } + } +} diff --git a/brouter-util/src/main/java/btools/util/LongList.java b/brouter-util/src/main/java/btools/util/LongList.java new file mode 100644 index 0000000..2da9914 --- /dev/null +++ b/brouter-util/src/main/java/btools/util/LongList.java @@ -0,0 +1,43 @@ +package btools.util; + +/** + * dynamic list of primitive longs + * + * @author ab + */ +public class LongList +{ + private long[] a; + private int size; + + public LongList( int capacity ) + { + a = capacity < 4 ? new long[4] : new long[capacity]; + } + + public void add( long value ) + { + if ( size == a.length ) + { + long[] aa = new long[2*size]; + System.arraycopy( a, 0, aa, 0, size ); + a = aa; + } + a[size++] = value; + } + + public long get( int idx ) + { + if ( idx >= size ) + { + throw new IndexOutOfBoundsException( "list size=" + size + " idx=" + idx ); + } + return a[idx]; + } + + public int size() + { + return size; + } + +} diff --git a/brouter-util/src/main/java/btools/util/TinyDenseLongMap.java b/brouter-util/src/main/java/btools/util/TinyDenseLongMap.java new file mode 100644 index 0000000..472b3c3 --- /dev/null +++ b/brouter-util/src/main/java/btools/util/TinyDenseLongMap.java @@ -0,0 +1,206 @@ +package btools.util; + +/** + * TinyDenseLongMap implements the DenseLongMap interface + * but actually is made for a medium count of non-dense keys + * + * It's used as a replacement for DenseLongMap where we + * have limited memory and far less keys than maykey + * + * @author ab + */ +public class TinyDenseLongMap extends DenseLongMap +{ + private long[][] al; + private int[] pa; + private int size = 0; + private int _maxKeepExponent = 14; // the maximum exponent to keep the invalid arrays + + protected static final int MAXLISTS = 31; // enough for size Integer.MAX_VALUE + + public TinyDenseLongMap() + { + super(1); + + // pointer array + pa = new int[MAXLISTS]; + + // allocate key lists + al = new long[MAXLISTS][]; + al[0] = new long[1]; // make the first array (the transient buffer) + + // same for the values + vla = new byte[MAXLISTS][]; + vla[0] = new byte[1]; + } + + + private byte[][] vla; // value list array + + private void fillReturnValue(byte[] rv, int idx, int p ) + { + rv[0] = vla[idx][p]; + if ( rv.length == 2 ) + { + vla[idx][p] = rv[1]; + } + } + + @Override + public void put( long id, int value ) + { + byte[] rv = new byte[2]; + rv[1] = (byte)value; + if ( contains( id, rv ) ) + { + return; + } + + vla[0][0] = (byte)value; + _add( id ); + } + + + /** + * Get the byte for the given id + * @param id the key to query + * @return the object + * @exception IllegalArgumentException if id is unknown + */ + @Override + public int getInt( long id ) + { + byte[] rv = new byte[1]; + if ( contains( id, rv ) ) + { + return rv[0]; + } + return -1; + } + + + private boolean _add( long id ) + { + if ( size == Integer.MAX_VALUE ) + { + throw new IllegalArgumentException( "cannot grow beyond size Integer.MAX_VALUE" ); + } + + // put the new entry in the first array + al[0][0] = id; + + // determine the first empty array + int bp = size++; // treat size as bitpattern + int idx = 1; + int n = 1; + + pa[0] = 1; + pa[1] = 1; + + while ( (bp&1) == 1 ) + { + bp >>= 1; + pa[idx++] = n; + n <<= 1; + } + + // create it if not existant + if ( al[idx] == null ) + { + al[idx] = new long[n]; + vla[idx] = new byte[n]; + } + + // now merge the contents of arrays 0...idx-1 into idx + while ( n > 0 ) + { + long maxId = 0; + int maxIdx = -1; + + for ( int i=0; i 0 ) + { + long currentId = al[i][p-1]; + if ( maxIdx < 0 || currentId > maxId ) + { + maxIdx = i; + maxId = currentId; + } + } + } + + // current maximum found, copy to target array + if ( n < al[idx].length && maxId == al[idx][n] ) + { + throw new IllegalArgumentException( "duplicate key found in late check: " + maxId ); + } + --n; + al[idx][n] = maxId; + vla[idx][n] = vla[maxIdx][pa[maxIdx]-1]; + + --pa[maxIdx]; + } + + // de-allocate empty arrays of a certain size (fix at 64kByte) + while ( idx-- > _maxKeepExponent ) + { + al[idx] = null; + vla[idx] = null; + } + + return false; + } + + + private boolean contains( long id, byte[] rv ) + { + // determine the first empty array + int bp = size; // treat size as bitpattern + int idx = 1; + + while ( bp != 0 ) + { + if ( (bp&1) == 1 ) + { + // array at idx is valid, check + if ( contains( idx, id, rv ) ) + { + return true; + } + } + idx++; + bp >>= 1; + } + return false; + } + + + // does sorted array "a" contain "id" ? + private boolean contains( int idx, long id, byte[] rv ) + { + long[] a = al[idx]; + int offset = a.length; + int n = 0; + + while ( (offset >>= 1) > 0 ) + { + int nn = n + offset; + if ( a[nn] <= id ) + { + n = nn; + } + } + if ( a[n] == id ) + { + if ( rv != null ) + { + fillReturnValue( rv, idx, n ); + } + return true; + } + return false; + } + +} diff --git a/brouter-util/src/test/java/btools/util/CompactLongMap.java b/brouter-util/src/test/java/btools/util/CompactLongMap.java new file mode 100644 index 0000000..314772b --- /dev/null +++ b/brouter-util/src/test/java/btools/util/CompactLongMap.java @@ -0,0 +1,328 @@ +package btools.util; + +import java.util.ArrayList; + +/** + * Memory efficient Map to map a long-key to an object-value + * + * Implementation is such that basically the 12 bytes + * per entry is allocated that's needed to store + * a long- and an object-value. + * This class does not implement the Map interface + * because it's not complete (remove() is not implemented, + * CompactLongMap can only grow.) + * + * @author ab + */ +public class CompactLongMap +{ + private long[][] al; + private int[] pa; + private int size = 0; + private int _maxKeepExponent = 14; // the maximum exponent to keep the invalid arrays + + private V value_in; + protected V value_out; + + protected static final int MAXLISTS = 31; // enough for size Integer.MAX_VALUE + private static boolean earlyDuplicateCheck; + + public CompactLongMap() + { + // pointer array + pa = new int[MAXLISTS]; + + // allocate key lists + al = new long[MAXLISTS][]; + al[0] = new long[1]; // make the first array (the transient buffer) + + // same for the values + vla = new Object[MAXLISTS][]; + vla[0] = new Object[1]; + + earlyDuplicateCheck = Boolean.getBoolean( "earlyDuplicateCheck" ); + } + + + /* + * + * The Map extension: + * next 5 protected methods are needed to implement value-support + * overwrite them all to support value structures other than the + * long-values implemented here as a sample. + * + * Furthermore, put() and get() method need to be implemented + * to access the values. + * + * Note that this map does not behave exactly like java.util.Map + * - put(..) with already existing key throws exception + * - get(..) with non-existing key thros exception + * + * If you have keys that cannot easily be mapped on long's, use + * a hash-function to do the mapping. But note that, in comparison + * to java.util.HashMap, in that case the keys itself are not saved, + * only the hash-values, so you need to be sure that random duplicate + * hashs are either excluded by the structure of your data or that + * you can handle the possible IllegalArgumentException + * + */ + + private Object[][] vla; // value list array + + + public boolean put( long id, V value ) + { + try + { + value_in = value; + if ( contains( id, true ) ) + { + return true; + } + vla[0][0] = value; + _add( id ); + return false; + } + finally + { + value_in = null; + value_out = null; + } + } + + /** + * Same as put( id, value ) but duplicate check + * is skipped for performance. Be aware that you + * can get a duplicate exception later on if the + * map is restructured! + * with System parameter earlyDuplicateCheck=true you + * can enforce the early duplicate check for debugging + * + * @param id the key to insert + * @param value the value to insert object + * @exception IllegalArgumentException for duplicates if enabled + */ + public void fastPut( long id, V value ) + { + if ( earlyDuplicateCheck && contains( id ) ) + { + throw new IllegalArgumentException( "duplicate key found in early check: " + id ); + } + vla[0][0] = value; + _add( id ); + } + + /** + * Get the value for the given id + * @param id the key to query + * @return the object, or null if id not known + */ + public V get( long id ) + { + try + { + if ( contains( id, false ) ) + { + return value_out; + } + return null; + } + finally + { + value_out = null; + } + } + + + /** + * @return the number of entries in this map + */ + public int size() + { + return size; + } + + + private boolean _add( long id ) + { + if ( size == Integer.MAX_VALUE ) + { + throw new IllegalArgumentException( "cannot grow beyond size Integer.MAX_VALUE" ); + } + + // put the new entry in the first array + al[0][0] = id; + + // determine the first empty array + int bp = size++; // treat size as bitpattern + int idx = 1; + int n = 1; + + pa[0] = 1; + pa[1] = 1; + + while ( (bp&1) == 1 ) + { + bp >>= 1; + pa[idx++] = n; + n <<= 1; + } + + // create it if not existant + if ( al[idx] == null ) + { + al[idx] = new long[n]; + vla[idx] = new Object[n]; + } + + // now merge the contents of arrays 0...idx-1 into idx + while ( n > 0 ) + { + long maxId = 0; + int maxIdx = -1; + + for ( int i=0; i 0 ) + { + long currentId = al[i][p-1]; + if ( maxIdx < 0 || currentId > maxId ) + { + maxIdx = i; + maxId = currentId; + } + } + } + + // current maximum found, copy to target array + if ( n < al[idx].length && maxId == al[idx][n] ) + { + throw new IllegalArgumentException( "duplicate key found in late check: " + maxId ); + } + --n; + al[idx][n] = maxId; + vla[idx][n] = vla[maxIdx][pa[maxIdx]-1]; + + --pa[maxIdx]; + } + + // de-allocate empty arrays of a certain size (fix at 64kByte) + while ( idx-- > _maxKeepExponent ) + { + al[idx] = null; + vla[idx] = null; + } + + return false; + } + + /** + * @return true if "id" is contained in this set. + */ + public boolean contains( long id ) + { + try + { + return contains( id, false ); + } + finally + { + value_out = null; + } + } + + protected boolean contains( long id, boolean doPut ) + { + // determine the first empty array + int bp = size; // treat size as bitpattern + int idx = 1; + + while ( bp != 0 ) + { + if ( (bp&1) == 1 ) + { + // array at idx is valid, check + if ( contains( idx, id, doPut ) ) + { + return true; + } + } + idx++; + bp >>= 1; + } + return false; + } + + + // does sorted array "a" contain "id" ? + private boolean contains( int idx, long id, boolean doPut ) + { + long[] a = al[idx]; + int offset = a.length; + int n = 0; + + while ( (offset >>= 1) > 0 ) + { + int nn = n + offset; + if ( a[nn] <= id ) + { + n = nn; + } + } + if ( a[n] == id ) + { + value_out = (V)vla[idx][n]; + if ( doPut ) vla[idx][n] = value_in; + return true; + } + return false; + } + + protected void moveToFrozenArrays( long[] faid, ArrayList flv ) + { + for( int i=1; i>= 1; + } + faid[ti] = minId; + flv.add( (V)vla[minIdx][pa[minIdx]] ); + pa[minIdx]++; + + if ( ti > 0 && faid[ti-1] == minId ) + { + throw new IllegalArgumentException( "duplicate key found in late check: " + minId ); + } + } + + // free the non-frozen arrays + al = null; + vla = null; + } + +} diff --git a/brouter-util/src/test/java/btools/util/CompactLongSet.java b/brouter-util/src/test/java/btools/util/CompactLongSet.java new file mode 100644 index 0000000..08dace4 --- /dev/null +++ b/brouter-util/src/test/java/btools/util/CompactLongSet.java @@ -0,0 +1,220 @@ +package btools.util; + +/** + * Memory efficient Set for long-keys + * + * @author ab + */ +public class CompactLongSet +{ + private long[][] al; + private int[] pa; + private int size = 0; + private int _maxKeepExponent = 14; // the maximum exponent to keep the invalid arrays + + protected static final int MAXLISTS = 31; // enough for size Integer.MAX_VALUE + private static boolean earlyDuplicateCheck; + + public CompactLongSet() + { + // pointer array + pa = new int[MAXLISTS]; + + // allocate key lists + al = new long[MAXLISTS][]; + al[0] = new long[1]; // make the first array (the transient buffer) + + earlyDuplicateCheck = Boolean.getBoolean( "earlyDuplicateCheck" ); + } + + + /** + * @return the number of entries in this set + */ + public int size() + { + return size; + } + + /** + * add a long value to this set if not yet in. + * @param id the value to add to this set. + * @return true if "id" already contained in this set. + */ + public boolean add( long id ) + { + if ( contains( id ) ) + { + return true; + } + _add( id ); + return false; + } + + public void fastAdd( long id ) + { + _add( id ); + } + + private void _add( long id ) + { + if ( size == Integer.MAX_VALUE ) + { + throw new IllegalArgumentException( "cannot grow beyond size Integer.MAX_VALUE" ); + } + + // put the new entry in the first array + al[0][0] = id; + + // determine the first empty array + int bp = size++; // treat size as bitpattern + int idx = 1; + int n = 1; + + pa[0] = 1; + pa[1] = 1; + + while ( (bp&1) == 1 ) + { + bp >>= 1; + pa[idx++] = n; + n <<= 1; + } + + // create it if not existant + if ( al[idx] == null ) + { + al[idx] = new long[n]; + } + + // now merge the contents of arrays 0...idx-1 into idx + while ( n > 0 ) + { + long maxId = 0; + int maxIdx = -1; + + for ( int i=0; i 0 ) + { + long currentId = al[i][p-1]; + if ( maxIdx < 0 || currentId > maxId ) + { + maxIdx = i; + maxId = currentId; + } + } + } + + // current maximum found, copy to target array + if ( n < al[idx].length && maxId == al[idx][n] ) + { + throw new IllegalArgumentException( "duplicate key found in late check: " + maxId ); + } + --n; + al[idx][n] = maxId; + + --pa[maxIdx]; + } + + // de-allocate empty arrays of a certain size (fix at 64kByte) + while ( idx-- > _maxKeepExponent ) + { + al[idx] = null; + } + } + + /** + * @return true if "id" is contained in this set. + */ + public boolean contains( long id ) + { + // determine the first empty array + int bp = size; // treat size as bitpattern + int idx = 1; + + while ( bp != 0 ) + { + if ( (bp&1) == 1 ) + { + // array at idx is valid, check + if ( contains( idx, id ) ) + { + return true; + } + } + idx++; + bp >>= 1; + } + return false; + } + + + // does sorted array "a" contain "id" ? + private boolean contains( int idx, long id ) + { + long[] a = al[idx]; + int offset = a.length; + int n = 0; + + while ( (offset >>= 1) > 0 ) + { + int nn = n + offset; + if ( a[nn] <= id ) + { + n = nn; + } + } + if ( a[n] == id ) + { + return true; + } + return false; + } + + protected void moveToFrozenArray( long[] faid ) + { + for( int i=1; i>= 1; + } + faid[ti] = minId; + pa[minIdx]++; + + if ( ti > 0 && faid[ti-1] == minId ) + { + throw new IllegalArgumentException( "duplicate key found in late check: " + minId ); + } + } + + // free the non-frozen array + al = null; + } + +} diff --git a/brouter-util/src/test/java/btools/util/DenseLongMap.java b/brouter-util/src/test/java/btools/util/DenseLongMap.java new file mode 100644 index 0000000..00b7dcc --- /dev/null +++ b/brouter-util/src/test/java/btools/util/DenseLongMap.java @@ -0,0 +1,140 @@ +package btools.util; + +import java.util.ArrayList; + +/** + * Special Memory efficient Map to map a long-key to + * a "small" value (some bits only) where it is expected + * that the keys are dense, so that we can use more or less + * a simple array as the best-fit data model (except for + * the 32-bit limit of arrays!) + * + * Additionally, to enable small-memory unit testing + * of code using this map this one has a fallback for a small map + * where we have only few keys, but not dense. In this + * case, we use the mechanics of the CompactLongMap + * + * Target application are osm-node ids which are in the + * range 0...3 billion and basically dense (=only few + * nodes deleted) + * + * @author ab + */ +public class DenseLongMap +{ + private ArrayList blocklist = new ArrayList(1024); + + private static final int BLOCKSIZE = 0x10000; // 64k * 32 bits + private int valuebits; + private int maxvalue; + private long maxkey; + private long maxmemory; + + /** + * Creates a DenseLongMap for the given value range + * Note that one value is reserved for the "unset" state, + * so with 6 value bits you can store values in the + * range 0..62 only + * + * @param valuebits number of bits to use per value + */ + public DenseLongMap( int valuebits ) + { + if ( valuebits < 1 || valuebits > 32 ) + { + throw new IllegalArgumentException( "invalid valuebits (1..32): " + valuebits ); + } + this.valuebits = valuebits; + maxmemory = (Runtime.getRuntime().maxMemory() / 8) * 7; // assume most of it for our map + maxvalue = (1 << valuebits) - 2; + maxkey = ( maxmemory / valuebits ) * 8; + } + + + + public void put( long key, int value ) + { + if ( key < 0L || key > maxkey ) + { + throw new IllegalArgumentException( "key out of range (0.." + maxkey + "): " + key + + " give more memory (currently " + (maxmemory / 0x100000) + + "MB) to extend key range" ); + } + if ( value < 0 || value > maxvalue ) + { + throw new IllegalArgumentException( "value out of range (0.." + maxvalue + "): " + value ); + } + + int blockn = (int)(key >> 21); + int offset = (int)(key & 0x1fffff); + + int[] block = blockn < blocklist.size() ? blocklist.get( blockn ) : null; + + if ( block == null ) + { + block = new int[BLOCKSIZE * valuebits]; + + while (blocklist.size() < blockn+1 ) + { + blocklist.add(null); + } + blocklist.set( blockn, block ); + } + + int bitmask = 1 << (offset & 0x1f); + int invmask = bitmask ^ 0xffffffff; + int probebit = 1; + int blockidx = (offset >> 5)*valuebits; + int blockend = blockidx + valuebits; + int v = value + 1; // 0 is reserved (=unset) + + while( blockidx < blockend ) + { + if ( ( v & probebit ) != 0 ) + { + block[blockidx] |= bitmask; + } + else + { + block[blockidx] &= invmask; + } + probebit <<= 1; + blockidx++; + } + } + + + public int getInt( long key ) + { + if ( key < 0 ) + { + return -1; + } + int blockn = (int)(key >> 21); + int offset = (int)(key & 0x1fffff); + + int[] block = blockn < blocklist.size() ? blocklist.get( blockn ) : null; + + if ( block == null ) + { + return -1; + } + int bitmask = 1 << (offset & 0x1f); + int probebit = 1; + int blockidx = (offset >> 5)*valuebits; + int blockend = blockidx + valuebits; + int v = 0; // 0 is reserved (=unset) + + while( blockidx < blockend ) + { + if ( ( block[blockidx] & bitmask ) != 0 ) + { + v |= probebit; + } + probebit <<= 1; + blockidx++; + } + return v-1; + } + +} diff --git a/brouter-util/src/test/java/btools/util/FrozenLongMap.java b/brouter-util/src/test/java/btools/util/FrozenLongMap.java new file mode 100644 index 0000000..b308173 --- /dev/null +++ b/brouter-util/src/test/java/btools/util/FrozenLongMap.java @@ -0,0 +1,122 @@ +package btools.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * Frozen instance of Memory efficient Map + * + * This one is readily sorted into a singe array for faster access + * + * @author ab + */ +public class FrozenLongMap extends CompactLongMap +{ + private long[] faid; + private ArrayList flv; + private int size = 0; + private int p2size; // next power of 2 of size + + public FrozenLongMap( CompactLongMap map ) + { + size = map.size(); + + faid = new long[size]; + flv = new ArrayList(size); + + map.moveToFrozenArrays( faid, flv ); + + p2size = 0x40000000; + while( p2size > size ) p2size >>= 1; + } + + @Override + public boolean put( long id, V value ) + { + throw new RuntimeException( "cannot put on FrozenLongIntMap" ); + } + + @Override + public void fastPut( long id, V value ) + { + throw new RuntimeException( "cannot put on FrozenLongIntMap" ); + } + + /** + * @return the number of entries in this set + */ + @Override + public int size() + { + return size; + } + + + + /** + * @return true if "id" is contained in this set. + */ + @Override + protected boolean contains( long id, boolean doPut ) + { + if ( size == 0 ) + { + return false; + } + long[] a = faid; + int offset = p2size; + int n = 0; + + while ( offset> 0 ) + { + int nn = n + offset; + if ( nn < size && a[nn] <= id ) + { + n = nn; + } + offset >>= 1; + } + if ( a[n] == id ) + { + value_out = flv.get(n); + return true; + } + return false; + } + + /** + * @return the value for "id", + * Throw an exception if not contained in the map. + */ + @Override + public V get( long id ) + { + if ( size == 0 ) + { + return null; + } + long[] a = faid; + int offset = p2size; + int n = 0; + + while ( offset> 0 ) + { + int nn = n + offset; + if ( nn < size && a[nn] <= id ) + { + n = nn; + } + offset >>= 1; + } + if ( a[n] == id ) + { + return flv.get(n); + } + return null; + } + + public List getValueList() + { + return flv; + } +} diff --git a/brouter-util/src/test/java/btools/util/FrozenLongSet.java b/brouter-util/src/test/java/btools/util/FrozenLongSet.java new file mode 100644 index 0000000..f6ef6b3 --- /dev/null +++ b/brouter-util/src/test/java/btools/util/FrozenLongSet.java @@ -0,0 +1,81 @@ +package btools.util; + +/** + * Frozen instance of Memory efficient Set + * + * This one is readily sorted into a singe array for faster access + * + * @author ab + */ +public class FrozenLongSet extends CompactLongSet +{ + private long[] faid; + private int size = 0; + private int p2size; // next power of 2 of size + + public FrozenLongSet( CompactLongSet set ) + { + size = set.size(); + + faid = new long[size]; + + set.moveToFrozenArray( faid ); + + p2size = 0x40000000; + while( p2size > size ) p2size >>= 1; + } + + @Override + public boolean add( long id ) + { + throw new RuntimeException( "cannot add on FrozenLongSet" ); + } + + @Override + public void fastAdd( long id ) + { + throw new RuntimeException( "cannot add on FrozenLongSet" ); + } + + /** + * @return the number of entries in this set + */ + @Override + public int size() + { + return size; + } + + + + /** + * @return true if "id" is contained in this set. + */ + @Override + public boolean contains( long id ) + { + if ( size == 0 ) + { + return false; + } + long[] a = faid; + int offset = p2size; + int n = 0; + + while ( offset> 0 ) + { + int nn = n + offset; + if ( nn < size && a[nn] <= id ) + { + n = nn; + } + offset >>= 1; + } + if ( a[n] == id ) + { + return true; + } + return false; + } + +} diff --git a/brouter-util/src/test/java/btools/util/LazyArrayOfLists.java b/brouter-util/src/test/java/btools/util/LazyArrayOfLists.java new file mode 100644 index 0000000..850504b --- /dev/null +++ b/brouter-util/src/test/java/btools/util/LazyArrayOfLists.java @@ -0,0 +1,53 @@ +package btools.util; + +import java.util.List; +import java.util.ArrayList; + +/** + * Behaves like an Array of list + * with lazy list-allocation at getList + * + * @author ab + */ +public class LazyArrayOfLists +{ + private ArrayList> lists; + + public LazyArrayOfLists( int size ) + { + lists = new ArrayList>( size ); + for ( int i = 0; i< size; i++ ) + { + lists.add( null ); + } + } + + public List getList( int idx ) + { + ArrayList list = lists.get( idx ); + if ( list == null ) + { + list = new ArrayList(); + lists.set( idx, list ); + } + return list; + } + + public int getSize( int idx ) + { + List list = lists.get( idx ); + return list == null ? 0 : list.size(); + } + + public void trimAll() + { + for ( int idx = 0; idx< lists.size(); idx++ ) + { + ArrayList list = lists.get( idx ); + if ( list != null ) + { + list.trimToSize(); + } + } + } +} diff --git a/brouter-util/src/test/java/btools/util/LongList.java b/brouter-util/src/test/java/btools/util/LongList.java new file mode 100644 index 0000000..2da9914 --- /dev/null +++ b/brouter-util/src/test/java/btools/util/LongList.java @@ -0,0 +1,43 @@ +package btools.util; + +/** + * dynamic list of primitive longs + * + * @author ab + */ +public class LongList +{ + private long[] a; + private int size; + + public LongList( int capacity ) + { + a = capacity < 4 ? new long[4] : new long[capacity]; + } + + public void add( long value ) + { + if ( size == a.length ) + { + long[] aa = new long[2*size]; + System.arraycopy( a, 0, aa, 0, size ); + a = aa; + } + a[size++] = value; + } + + public long get( int idx ) + { + if ( idx >= size ) + { + throw new IndexOutOfBoundsException( "list size=" + size + " idx=" + idx ); + } + return a[idx]; + } + + public int size() + { + return size; + } + +} diff --git a/brouter-util/src/test/java/btools/util/TinyDenseLongMap.java b/brouter-util/src/test/java/btools/util/TinyDenseLongMap.java new file mode 100644 index 0000000..472b3c3 --- /dev/null +++ b/brouter-util/src/test/java/btools/util/TinyDenseLongMap.java @@ -0,0 +1,206 @@ +package btools.util; + +/** + * TinyDenseLongMap implements the DenseLongMap interface + * but actually is made for a medium count of non-dense keys + * + * It's used as a replacement for DenseLongMap where we + * have limited memory and far less keys than maykey + * + * @author ab + */ +public class TinyDenseLongMap extends DenseLongMap +{ + private long[][] al; + private int[] pa; + private int size = 0; + private int _maxKeepExponent = 14; // the maximum exponent to keep the invalid arrays + + protected static final int MAXLISTS = 31; // enough for size Integer.MAX_VALUE + + public TinyDenseLongMap() + { + super(1); + + // pointer array + pa = new int[MAXLISTS]; + + // allocate key lists + al = new long[MAXLISTS][]; + al[0] = new long[1]; // make the first array (the transient buffer) + + // same for the values + vla = new byte[MAXLISTS][]; + vla[0] = new byte[1]; + } + + + private byte[][] vla; // value list array + + private void fillReturnValue(byte[] rv, int idx, int p ) + { + rv[0] = vla[idx][p]; + if ( rv.length == 2 ) + { + vla[idx][p] = rv[1]; + } + } + + @Override + public void put( long id, int value ) + { + byte[] rv = new byte[2]; + rv[1] = (byte)value; + if ( contains( id, rv ) ) + { + return; + } + + vla[0][0] = (byte)value; + _add( id ); + } + + + /** + * Get the byte for the given id + * @param id the key to query + * @return the object + * @exception IllegalArgumentException if id is unknown + */ + @Override + public int getInt( long id ) + { + byte[] rv = new byte[1]; + if ( contains( id, rv ) ) + { + return rv[0]; + } + return -1; + } + + + private boolean _add( long id ) + { + if ( size == Integer.MAX_VALUE ) + { + throw new IllegalArgumentException( "cannot grow beyond size Integer.MAX_VALUE" ); + } + + // put the new entry in the first array + al[0][0] = id; + + // determine the first empty array + int bp = size++; // treat size as bitpattern + int idx = 1; + int n = 1; + + pa[0] = 1; + pa[1] = 1; + + while ( (bp&1) == 1 ) + { + bp >>= 1; + pa[idx++] = n; + n <<= 1; + } + + // create it if not existant + if ( al[idx] == null ) + { + al[idx] = new long[n]; + vla[idx] = new byte[n]; + } + + // now merge the contents of arrays 0...idx-1 into idx + while ( n > 0 ) + { + long maxId = 0; + int maxIdx = -1; + + for ( int i=0; i 0 ) + { + long currentId = al[i][p-1]; + if ( maxIdx < 0 || currentId > maxId ) + { + maxIdx = i; + maxId = currentId; + } + } + } + + // current maximum found, copy to target array + if ( n < al[idx].length && maxId == al[idx][n] ) + { + throw new IllegalArgumentException( "duplicate key found in late check: " + maxId ); + } + --n; + al[idx][n] = maxId; + vla[idx][n] = vla[maxIdx][pa[maxIdx]-1]; + + --pa[maxIdx]; + } + + // de-allocate empty arrays of a certain size (fix at 64kByte) + while ( idx-- > _maxKeepExponent ) + { + al[idx] = null; + vla[idx] = null; + } + + return false; + } + + + private boolean contains( long id, byte[] rv ) + { + // determine the first empty array + int bp = size; // treat size as bitpattern + int idx = 1; + + while ( bp != 0 ) + { + if ( (bp&1) == 1 ) + { + // array at idx is valid, check + if ( contains( idx, id, rv ) ) + { + return true; + } + } + idx++; + bp >>= 1; + } + return false; + } + + + // does sorted array "a" contain "id" ? + private boolean contains( int idx, long id, byte[] rv ) + { + long[] a = al[idx]; + int offset = a.length; + int n = 0; + + while ( (offset >>= 1) > 0 ) + { + int nn = n + offset; + if ( a[nn] <= id ) + { + n = nn; + } + } + if ( a[n] == id ) + { + if ( rv != null ) + { + fillReturnValue( rv, idx, n ); + } + return true; + } + return false; + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6ca1e23 --- /dev/null +++ b/pom.xml @@ -0,0 +1,153 @@ + + + 4.0.0 + org.btools + brouter + 0.98 + pom + http://brensche.de/brouter/ + brouter + The brouter project provides bike routing for OpenStreetMap. + + + brouter-util + brouter-expressions + brouter-mapaccess + brouter-core + brouter-map-creator + brouter-server + brouter-routing-app + + + + scm:svn:http://www.routeconverter.de/subversion/BRouter/ + scm:svn:http://www.routeconverter.de/subversion/BRouter/ + http://www.routeconverter.de/subversion/BRouter/ + HEAD + + + + + arndt.brenschede + Arndt Brenschede + Arndt.Brenschede@web.de + + + + + UTF-8 + UTF-8 + 1.6 + + + + + + + com.jayway.maven.plugins.android.generation2 + android-maven-plugin + 3.6.0 + true + + gen + gen + true + + 10 + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.4 + + + jar-with-dependencies + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${targetJdk} + ${targetJdk} + -Xlint:unchecked + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9 + + false + brensche.de/brouter]]> + true + protected + false + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadoc + package + + jar + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + package + + jar-no-fork + + + + + + + + + + + junit + junit + 4.11 + test + + + + +