/**
* 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.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import btools.mapaccess.OsmPos;
import btools.util.CompactLongMap;
import btools.util.FrozenLongMap;
public final class OsmTrack
{
// csv-header-line
private static final String MESSAGES_HEADER = "Longitude\tLatitude\tElevation\tDistance\tCostPerKm\tElevCost\tTurnCost\tNodeCost\tInitialCost\tWayTags\tNodeTags";
public MatchedWaypoint endPoint;
public long[] nogoChecksums;
public long profileTimestamp;
public boolean isDirty;
private static class OsmPathElementHolder
{
public OsmPathElement node;
public OsmPathElementHolder nextHolder;
}
public ArrayList<OsmPathElement> nodes = new ArrayList<OsmPathElement>();
private CompactLongMap<OsmPathElementHolder> nodesMap;
private CompactLongMap<OsmPathElementHolder> detourMap;
private VoiceHintList voiceHints;
public String message = null;
public ArrayList<String> messageList = null;
public String name = "unset";
public void addNode( OsmPathElement node )
{
nodes.add( 0, node );
}
public void registerDetourForId( long id, OsmPathElement detour )
{
if ( detourMap == null )
{
detourMap = new CompactLongMap<OsmPathElementHolder>();
}
OsmPathElementHolder nh = new OsmPathElementHolder();
nh.node = detour;
OsmPathElementHolder h = detourMap.get( id );
if ( h != null )
{
while ( h.nextHolder != null )
{
h = h.nextHolder;
}
h.nextHolder = nh;
}
else
{
detourMap.fastPut( id, nh );
}
}
public void copyDetours( OsmTrack source )
{
detourMap = source.detourMap == null ? null : new FrozenLongMap<OsmPathElementHolder>( source.detourMap );
}
public void buildMap()
{
nodesMap = new CompactLongMap<OsmPathElementHolder>();
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<OsmPathElementHolder>( nodesMap );
}
private ArrayList<String> aggregateMessages()
{
ArrayList<String> res = new ArrayList<String>();
MessageData current = null;
for ( OsmPathElement n : nodes )
{
if ( n.message != null )
{
MessageData md = n.message.copy();
if ( current != null )
{
if ( current.nodeKeyValues != null || !current.wayKeyValues.equals( md.wayKeyValues ) )
{
res.add( current.toMessage() );
}
else
{
md.add( current );
}
}
current = md;
}
}
if ( current != null )
{
res.add( current.toMessage() );
}
return res;
}
/**
* 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.writeLong( nogoChecksums[0] );
dos.writeLong( nogoChecksums[1] );
dos.writeLong( nogoChecksums[2] );
dos.writeBoolean( isDirty );
dos.writeLong( profileTimestamp );
dos.close();
}
public static OsmTrack readBinary( String filename, OsmNodeNamed newEp, long[] nogoChecksums, long profileChecksum, StringBuilder debugInfo )
{
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;
boolean targetMatch = dlon < 20 && dlon > -20 && dlat < 20 && dlat > -20;
if ( debugInfo != null )
{
debugInfo.append( "target-delta = " + dlon + "/" + dlat + " targetMatch=" + targetMatch );
}
if ( targetMatch )
{
t = new OsmTrack();
t.endPoint = ep;
int n = dis.readInt();
OsmPathElement last_pe = null;
for ( int i = 0; i < n; i++ )
{
OsmPathElement pe = OsmPathElement.readFromStream( dis );
pe.origin = last_pe;
last_pe = pe;
t.nodes.add( pe );
}
t.cost = last_pe.cost;
t.buildMap();
// check cheecksums, too
long[] al = new long[3];
long pchecksum = 0;
try
{
al[0] = dis.readLong();
al[1] = dis.readLong();
al[2] = dis.readLong();
}
catch (EOFException eof) { /* kind of expected */ }
try
{
t.isDirty = dis.readBoolean();
}
catch (EOFException eof) { /* kind of expected */ }
try
{
pchecksum = dis.readLong();
}
catch (EOFException eof) { /* kind of expected */ }
boolean nogoCheckOk = Math.abs( al[0] - nogoChecksums[0] ) <= 20
&& Math.abs( al[1] - nogoChecksums[1] ) <= 20
&& Math.abs( al[2] - nogoChecksums[2] ) <= 20;
boolean profileCheckOk = pchecksum == profileChecksum;
if ( debugInfo != null )
{
debugInfo.append( " nogoCheckOk=" + nogoCheckOk + " profileCheckOk=" + profileCheckOk );
debugInfo.append( " al=" + formatLongs(al) + " nogoChecksums=" + formatLongs(nogoChecksums) );
}
if ( !(nogoCheckOk && profileCheckOk) ) return null;
}
dis.close();
}
catch (Exception e)
{
throw new RuntimeException( "Exception reading rawTrack: " + e );
}
}
}
return t;
}
private static String formatLongs( long[] al )
{
StringBuilder sb = new StringBuilder();
sb.append( '{' );
for( long l : al )
{
sb.append( l );
sb.append( ' ' );
}
sb.append( '}' );
return sb.toString();
}
public void addNodes( OsmTrack t )
{
for ( OsmPathElement n : t.nodes )
addNode( n );
buildMap();
}
public boolean containsNode( OsmPos node )
{
return nodesMap.contains( node.getIdFromPos() );
}
public OsmPathElement getLink( long n1, long n2 )
{
OsmPathElementHolder h = nodesMap.get( n2 );
while (h != null)
{
OsmPathElement e1 = h.node.origin;
if ( e1 != null && e1.getIdFromPos() == n1 )
{
return h.node;
}
h = h.nextHolder;
}
return null;
}
public void appendTrack( OsmTrack t )
{
for ( int i = 0; i < t.nodes.size(); i++ )
{
if ( i > 0 || nodes.size() == 0 )
{
nodes.add( t.nodes.get( i ) );
}
}
if ( t.voiceHints != null )
{
if ( voiceHints == null )
{
voiceHints = t.voiceHints;
}
else
{
voiceHints.list.addAll( t.voiceHints.list );
}
}
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 ) );
formatAsGpx( bw );
bw.close();
}
public String formatAsGpx()
{
try
{
StringWriter sw = new StringWriter( 8192 );
BufferedWriter bw = new BufferedWriter( sw );
formatAsGpx( bw );
bw.close();
return sw.toString();
}
catch( Exception e )
{
throw new RuntimeException( e );
}
}
public String formatAsGpx( BufferedWriter sb ) throws IOException
{
int turnInstructionMode = voiceHints != null ? voiceHints.turnInstructionMode : 0;
sb.append( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\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( "<!-- " ).append( message ).append( " -->\n" );
}
if ( turnInstructionMode == 4 ) // comment style
{
sb.append( "<!-- $transport-mode$").append( voiceHints.getTransportMode() ).append( "$ -->\n" );
sb.append( "<!-- cmd idx lon lat d2next geometry -->\n" );
sb.append( "<!-- $turn-instruction-start$\n" );
for( VoiceHint hint: voiceHints.list )
{
sb.append( String.format( " $turn$%6s;%6d;%10s;%10s;%6d;%s$\n", hint.getCommandString(), hint.indexInTrack,
formatILon( hint.ilon ), formatILat( hint.ilat ), (int)(hint.distanceToNext), hint.formatGeometry() ) );
}
sb.append( " $turn-instruction-end$ -->\n" );
}
sb.append( "<gpx \n" );
sb.append( " xmlns=\"http://www.topografix.com/GPX/1/1\" \n" );
sb.append( " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" \n" );
if ( turnInstructionMode == 2 ) // locus style
{
sb.append( " xmlns:locus=\"http://www.locusmap.eu\" \n" );
}
sb.append( " xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\" \n" );
if ( turnInstructionMode == 3)
{
sb.append(" creator=\"OsmAndRouter\" version=\"1.1\">\n" );
}
else
{
sb.append( " creator=\"BRouter-1.4.8\" version=\"1.1\">\n" );
}
if ( turnInstructionMode == 3) // osmand style
{
sb.append(" <rte>\n");
for( VoiceHint hint: voiceHints.list )
{
sb.append(" <rtept lat=\"").append( formatILat( hint.ilat ) ).append( "\" lon=\"" )
.append( formatILon( hint.ilon ) ).append( "\">\n" )
.append ( " <desc>" ).append( hint.getMessageString() ).append( "</desc>\n <extensions>\n <turn>" )
.append( hint.getCommandString() ).append("</turn>\n <turn-angle>").append( "" + hint.angle )
.append("</turn-angle>\n <offset>").append( "" + hint.indexInTrack ).append("</offset>\n </extensions>\n </rtept>\n");
}
sb.append("</rte>\n");
}
if ( turnInstructionMode == 2 ) // locus style
{
for( VoiceHint hint: voiceHints.list )
{
sb.append( " <wpt lon=\"" ).append( formatILon( hint.ilon ) ).append( "\" lat=\"" )
.append( formatILat( hint.ilat ) ).append( "\">" )
.append( hint.selev == Short.MIN_VALUE ? "" : "<ele>" + (hint.selev / 4.) + "</ele>" )
.append( "<name>" ).append( hint.getMessageString() ).append( "</name>" )
.append( "<extensions><locus:rteDistance>" ).append( "" + hint.distanceToNext ).append( "</locus:rteDistance>" )
.append( "<locus:rtePointAction>" ).append( "" + hint.getLocusAction() ).append( "</locus:rtePointAction></extensions>" )
.append( "</wpt>\n" );
}
}
if ( turnInstructionMode == 5 ) // gpsies style
{
for( VoiceHint hint: voiceHints.list )
{
sb.append( " <wpt lon=\"" ).append( formatILon( hint.ilon ) ).append( "\" lat=\"" )
.append( formatILat( hint.ilat ) ).append( "\">" )
.append( "<name>" ).append( hint.getMessageString() ).append( "</name>" )
.append( "<sym>" ).append( hint.getSymbolString().toLowerCase() ).append( "</sym>" )
.append( "<type>" ).append( hint.getSymbolString() ).append( "</type>" )
.append( "</wpt>\n" );
}
}
sb.append( " <trk>\n" );
sb.append( " <name>" ).append( name ).append( "</name>\n" );
if ( turnInstructionMode == 1 ) // trkpt/sym style
{
sb.append( " <type>" ).append( voiceHints.getTransportMode() ).append( "</type>\n" );
}
if ( turnInstructionMode == 2 )
{
sb.append( " <extensions><locus:rteComputeType>" ).append( "" + voiceHints.getLocusRouteType() ).append( "</locus:rteComputeType></extensions>\n" );
sb.append( " <extensions><locus:rteSimpleRoundabouts>1</locus:rteSimpleRoundabouts></extensions>\n" );
}
sb.append( " <trkseg>\n" );
for ( int idx = 0; idx < nodes.size(); idx++ )
{
OsmPathElement n = nodes.get(idx);
String sele = n.getSElev() == Short.MIN_VALUE ? "" : "<ele>" + n.getElev() + "</ele>";
if ( turnInstructionMode == 1 ) // trkpt/sym style
{
for ( VoiceHint hint : voiceHints.list )
{
if ( hint.indexInTrack == idx )
{
sele += "<sym>" + hint.getCommandString() + "</sym>";
}
}
}
sb.append( " <trkpt lon=\"" ).append( formatILon( n.getILon() ) ).append( "\" lat=\"" )
.append( formatILat( n.getILat() ) ).append( "\">" ).append( sele ).append( "</trkpt>\n" );
}
sb.append( " </trkseg>\n" );
sb.append( " </trk>\n" );
sb.append( "</gpx>\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( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" );
sb.append( "<kml xmlns=\"http://earth.google.com/kml/2.0\">\n" );
sb.append( " <Document>\n" );
sb.append( " <name>KML Samples</name>\n" );
sb.append( " <open>1</open>\n" );
sb.append( " <distance>3.497064</distance>\n" );
sb.append( " <traveltime>872</traveltime>\n" );
sb.append( " <description>To enable simple instructions add: 'instructions=1' as parameter to the URL</description>\n" );
sb.append( " <Folder>\n" );
sb.append( " <name>Paths</name>\n" );
sb.append( " <visibility>0</visibility>\n" );
sb.append( " <description>Examples of paths.</description>\n" );
sb.append( " <Placemark>\n" );
sb.append( " <name>Tessellated</name>\n" );
sb.append( " <visibility>0</visibility>\n" );
sb.append( " <description><![CDATA[If the <tessellate> tag has a value of 1, the line will contour to the underlying terrain]]></description>\n" );
sb.append( " <LineString>\n" );
sb.append( " <tessellate>1</tessellate>\n" );
sb.append( " <coordinates> " );
for ( OsmPathElement n : nodes )
{
sb.append( formatILon( n.getILon() ) ).append( "," ).append( formatILat( n.getILat() ) ).append( "\n" );
}
sb.append( " </coordinates>\n" );
sb.append( " </LineString>\n" );
sb.append( " </Placemark>\n" );
sb.append( " </Folder>\n" );
sb.append( " </Document>\n" );
sb.append( "</kml>\n" );
return sb.toString();
}
public List<String> iternity;
public String formatAsGeoJson()
{
StringBuilder sb = new StringBuilder( 8192 );
sb.append( "{\n" );
sb.append( " \"type\": \"FeatureCollection\",\n" );
sb.append( " \"features\": [\n" );
sb.append( " {\n" );
sb.append( " \"type\": \"Feature\",\n" );
sb.append( " \"properties\": {\n" );
sb.append( " \"creator\": \"BRouter-1.1\",\n" );
sb.append( " \"name\": \"" ).append( name ).append( "\",\n" );
sb.append( " \"track-length\": \"" ).append( distance ).append( "\",\n" );
sb.append( " \"filtered ascend\": \"" ).append( ascend ).append( "\",\n" );
sb.append( " \"plain-ascend\": \"" ).append( plainAscend ).append( "\",\n" );
sb.append( " \"cost\": \"" ).append( cost ).append( "\",\n" );
sb.append( " \"messages\": [\n" );
sb.append( " [\"" ).append( MESSAGES_HEADER.replaceAll( "\t", "\", \"" ) ).append( "\"],\n" );
for ( String m : aggregateMessages() )
{
sb.append( " [\"" ).append( m.replaceAll( "\t", "\", \"" ) ).append( "\"],\n" );
}
sb.deleteCharAt( sb.lastIndexOf( "," ) );
sb.append( " ]\n" );
sb.append( " },\n" );
if ( iternity != null )
{
sb.append( " \"iternity\": [\n" );
for ( String s : iternity )
{
sb.append( " \"" ).append( s ).append( "\",\n" );
}
sb.deleteCharAt( sb.lastIndexOf( "," ) );
sb.append( " ],\n" );
}
sb.append( " \"geometry\": {\n" );
sb.append( " \"type\": \"LineString\",\n" );
sb.append( " \"coordinates\": [\n" );
for ( OsmPathElement n : nodes )
{
String sele = n.getSElev() == Short.MIN_VALUE ? "" : ", " + n.getElev();
sb.append( " [" ).append( formatILon( n.getILon() ) ).append( ", " ).append( formatILat( n.getILat() ) )
.append( sele ).append( "],\n" );
}
sb.deleteCharAt( sb.lastIndexOf( "," ) );
sb.append( " ]\n" );
sb.append( " }\n" );
sb.append( " }\n" );
sb.append( " ]\n" );
sb.append( "}\n" );
return sb.toString();
}
private static String formatILon( int ilon )
{
return formatPos( ilon - 180000000 );
}
private static String formatILat( int ilat )
{
return formatPos( ilat - 90000000 );
}
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 ) );
writeMessages( bw, rc );
}
public void writeMessages( BufferedWriter bw, RoutingContext rc ) throws Exception
{
dumpLine( bw, MESSAGES_HEADER );
for ( String m : aggregateMessages() )
{
dumpLine( bw, m );
}
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( "<trkpt lon=\"" );
if ( idx0 >= 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( OsmPathElement.create( ilon, ilat, (short) 0, null, false ) );
}
}
br.close();
}
public boolean equalsTrack( OsmTrack t )
{
if ( nodes.size() != t.nodes.size() )
return false;
for ( int i = 0; i < nodes.size(); i++ )
{
OsmPathElement e1 = nodes.get( i );
OsmPathElement e2 = t.nodes.get( i );
if ( e1.getILon() != e2.getILon() || e1.getILat() != e2.getILat() )
return false;
}
return true;
}
public void processVoiceHints( RoutingContext rc )
{
voiceHints = new VoiceHintList();
voiceHints.setTransportMode( rc.carMode, rc.bikeMode );
voiceHints.turnInstructionMode = rc.turnInstructionMode;
if ( detourMap == null )
{
return;
}
int nodeNr = nodes.size() - 1;
OsmPathElement node = nodes.get( nodeNr );
List<VoiceHint> inputs = new ArrayList<VoiceHint>();
while (node != null)
{
if ( node.origin != null )
{
VoiceHint input = new VoiceHint();
inputs.add( input );
input.ilat = node.origin.getILat();
input.ilon = node.origin.getILon();
input.selev = node.origin.getSElev();
input.indexInTrack = --nodeNr;
input.goodWay = node.message;
input.oldWay = node.origin.message == null ? node.message : node.origin.message;
OsmPathElementHolder detours = detourMap.get( node.origin.getIdFromPos() );
if ( detours != null )
{
OsmPathElementHolder h = detours;
while (h != null)
{
OsmPathElement e = h.node;
input.addBadWay( startSection( e, node.origin ) );
h = h.nextHolder;
}
}
}
node = node.origin;
}
VoiceHintProcessor vproc = new VoiceHintProcessor( rc.turnInstructionCatchingRange, rc.turnInstructionRoundabouts );
List<VoiceHint> results = vproc.process( inputs );
for( VoiceHint hint : results )
{
voiceHints.list.add( hint );
}
}
private MessageData startSection( OsmPathElement element, OsmPathElement root )
{
OsmPathElement e = element;
int cnt = 0;
while( e != null && e.origin != null )
{
if ( e.origin.getILat() == root.getILat() && e.origin.getILon() == root.getILon() )
{
return e.message;
}
e = e.origin;
if ( cnt++ == 1000000 )
{
throw new IllegalArgumentException( "ups: " + root + "->" + element );
}
}
return null;
}
}