/*
This file is part of RouteConverter.
RouteConverter is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
RouteConverter is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with RouteConverter; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Copyright (C) 2007 Christian Pesch. All Rights Reserved.
*/
package slash.navigation.nmn;
import slash.navigation.base.*;
import slash.navigation.common.NavigationPosition;
import java.io.*;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
import static slash.common.io.Transfer.UTF8_ENCODING;
import static slash.common.io.Transfer.toMixedCase;
import static slash.navigation.base.RouteCalculations.asWgs84Position;
import static slash.navigation.base.RouteCharacteristics.Route;
/**
* Reads and writes Navigon Mobile Navigator (.route) files.
*
* @author Malte Neumann
*/
public class NmnRouteFormat extends SimpleFormat<Wgs84Route> {
private static final Preferences preferences = Preferences.userNodeForPackage(NmnRouteFormat.class);
private static final Logger log = Logger.getLogger(NmnRouteFormat.class.getName());
public static final int START_BYTES = 0xFFFF;
public static final long UNKNOWN_START_BYTES = 1L;
public String getExtension() {
return ".route";
}
public String getName() {
return "Navigon Mobile Navigator (*" + getExtension() + ")";
}
public int getMaximumPositionCount() {
return preferences.getInt("maximumNavigonRoutePositionCount", 99);
}
@SuppressWarnings({"unchecked"})
public <P extends NavigationPosition> Wgs84Route createRoute(RouteCharacteristics characteristics, String name, List<P> positions) {
return new Wgs84Route(this, characteristics, (List<Wgs84Position>) positions);
}
public void read(BufferedReader reader, String encoding, ParserContext<Wgs84Route> context) throws IOException {
// this format parses the InputStream directly but wants to derive from SimpleFormat to use Wgs84Route
throw new UnsupportedOperationException();
}
/*
Kodierung: Little Endian
Dateibeginn ff ff 00 00
Gefolgt 01 00 00 00 00 00 00 00 (1 als 64 bit)
4 Byte Länge bis Ende. diese 4 Byte inkludiert. Rest also Wert - 4
4 Byte 0
4 Byte Länge der folgenden Nutzdaten
Nutzdaten hier Datum
4 Byte Anzahl Punkte
4 Byte int gesehen 0 und 1
1..n Punkt
4 Byte Länge des folgenden Punktes. diese 4 Byte nicht mitzählen
8 Byte 0
4 Byte int Anzahl der folgenden Buchstaben
n Byte Text
4 Byte int bisher nur 1
4 Byte int Anzahl der folgenden Datenpunkte mit 04
8 Byte unterschiedlichster Inhalt. 1. 4 Byte Unixtimestamp. 1. 4 Byte Unixtimestamp. Ab zweiten Punkt 0
4 Byte (int )Datentyp bisher gesehen: 0, 1, 4. 0 immer nach Datentyp 4
Sind Länge Bytes gelesen kommt der nächste Punkt in identischem Aufbau
Datentyp 4 (04 00 00 00)
4 byte int 04 00 00 00
8 byte int als Länge diese nicht mitzählen
4 byte int Länge des Textes. Wenn 0, dann folgende trotzdem 4 Bytes.
n byte Text
manchmal hier schon zu Ende.
4 byte (int) bisher gefunden: 0, 1
8 byte breite double
8 byte länge double
4 byte int bisher gefunden 2, 3,
4 byte int abhängig folgt bis zur Gesamtlänge - 8
0x0: 4 byte int dann:
00 7B A4 3F:
4 byte textlänge
n byte Text
05 00 00 00:
5 byte text
0x2: 8 byte ?
0x7:
0x9: 4 byte Textlänge
n byte Text
0x8: 4 byte Textlänge
n byte Text
0x32: es folgt
4 byte Textlänge
n byte PLZ
0x3C: es folgt
4 byte Textlänge
n byte Text
4 byte
4 byte Textlänge
n byte Text
4 byte Textlänge
n byte Text
8 byte ?
*/
private boolean checkHeader(InputStream source) throws IOException {
byte[] headerBytes = new byte[16];
if (source.read(headerBytes) != headerBytes.length)
throw new IOException("Could not read " + headerBytes.length + " bytes");
ByteBuffer headerBuffer = ByteBuffer.wrap(headerBytes);
headerBuffer.order(LITTLE_ENDIAN);
headerBuffer.position(0);
if (headerBuffer.getInt() == START_BYTES && headerBuffer.getLong() == UNKNOWN_START_BYTES) {
long fileSize = headerBuffer.getInt();
return source.available() == fileSize - 4;
}
return false;
}
private String getText(ByteBuffer byteBuffer) throws EOFException {
int textLen = byteBuffer.getInt();
if (textLen > byteBuffer.capacity() - byteBuffer.position())
throw new EOFException();
return getText(byteBuffer, textLen);
}
private String getText(ByteBuffer byteBuffer, int count) throws EOFException {
if (byteBuffer.position() + count > byteBuffer.capacity())
throw new EOFException();
if (count > 0) {
byte[] text = new byte[count];
for (int i = 0; i < text.length; i++)
text[i] = byteBuffer.get();
try {
return toMixedCase(new String(text, UTF8_ENCODING));
} catch (UnsupportedEncodingException e) {
return "?";
}
}
return "";
}
@SuppressWarnings({"UnusedDeclaration"})
public void read(InputStream source, ParserContext<Wgs84Route> context) throws Exception {
if (checkHeader(source)) {
// copy whole file to a bytebuffer
byte[] bodyBytes = new byte[source.available()];
if (source.read(bodyBytes) != bodyBytes.length)
throw new IOException("Could not read " + bodyBytes.length + " bytes");
ByteBuffer fileContent = ByteBuffer.wrap(bodyBytes);
fileContent.order(LITTLE_ENDIAN);
fileContent.position(0);
// 4 Byte: position count - always 0?
fileContent.getInt();
// 4 Byte: length + creation date
try {
getText(fileContent);
}
catch (EOFException e) {
// can't read the first text --> wrong format
return;
}
// 4 Byte: expected position count
int expectedPositionCount = fileContent.getInt();
// 4 Byte: unknown - seen: 0, 1
int unknown = fileContent.getInt();
if (unknown != 0 && unknown != 1) {
log.fine("Unknown 13-16: seen " + unknown + ", not expected 0 or 1");
}
List<NavigationPosition> positions = new ArrayList<>();
int readPositions = 0;
//Ws ist möglich, dass bei einer "Position" überhaupt keine Koordinaten da
//sind. Dieser Punkt muss trotzdem am Ende mitgezählt werden für die Anzahl.
//Daher nicht am Ende positions.size() == expectedPositionCount testen
while (fileContent.position() < fileContent.capacity() - 4) {
Wgs84Position position = readPosition(fileContent);
if (position != null)
positions.add(position);
readPositions++;
}
if (readPositions == expectedPositionCount)
context.appendRoute(createRoute(Route, null, positions));
}
}
@SuppressWarnings({"UnusedDeclaration"})
protected Wgs84Position readPosition(ByteBuffer fileContent) throws EOFException {
// 4 Byte length
int positionLength = fileContent.getInt();
int positionEndPosition = positionLength + fileContent.position();
try {
// 8 Byte 0. unknown
fileContent.position(fileContent.position() + 8);
// 4 Byte: length + text
String text = getText(fileContent);
// 4 Byte: unknown
fileContent.getInt();
// 4 Byte: number of following data points (1, 2, 4)
fileContent.getInt();
// 8 Byte: unknown
if (fileContent.position() < positionEndPosition)
fileContent.getInt();
if (fileContent.position() < positionEndPosition)
fileContent.getInt();
Wgs84Position position = null;
int countBlock = 0;
while (fileContent.position() < positionEndPosition) {
int blockType = fileContent.getInt();
if (blockType == 1) {
position = readBlocktype_01(fileContent, position);
countBlock++;
} else if (blockType == 2) {
position = readBlocktype_02(fileContent, position, countBlock++);
// nur die ersten beiden Blöcke lesen. Danach kommt nur noch Bundesland, Land und Unbekanntes
if (countBlock > 2)
fileContent.position(positionEndPosition);
} else if (blockType == 4) {
position = readBlocktype_04(fileContent, position, countBlock++);
// nur die ersten beiden Blöcke lesen. Danach kommt nur noch Bundesland, Land und Unbekanntes
if (countBlock > 2)
fileContent.position(positionEndPosition);
} else if (blockType == 0) {
position = readBlocktype_00(fileContent, position, countBlock++);
// nur die ersten beiden Blöcke lesen. Danach kommt nur noch Bundesland, Land und Unbekanntes
if (countBlock > 2)
fileContent.position(positionEndPosition);
}
}
return position;
} catch (EOFException e) {
fileContent.position(positionEndPosition);
return null;
}
}
protected Wgs84Position readBlocktype_00(ByteBuffer byteBuffer, Wgs84Position positionPoint, int segmentCount) throws EOFException {
/*
4 byte in 00 00 00 00
8 byte int Länge. diese 8 bytes nicht mitzählen
4 byte Textlänge
n byte Text
8 byte breite double
8 byte länge double
4 byte int anzahl der noch folgenden Beschreibungen?
wiederholungen bis zum Ende - 12
4 byte int typ
4 byte int textlänge
n byte Text
*/
long blockLength = byteBuffer.getLong();
if ((byteBuffer.remaining() == 0) || (blockLength == 0))
return positionPoint;
int startPosition = byteBuffer.position();
String waypointDescription = getText(byteBuffer);
double longitude = 0;
double latitude = 0;
if (byteBuffer.position() < startPosition + blockLength - 8) {
longitude = byteBuffer.getDouble();
latitude = byteBuffer.getDouble();
}
//go to end of point. we have all needed data (text, position)
byteBuffer.position((int) (startPosition + blockLength));
Wgs84Position resultPoint;
if (positionPoint == null) {
resultPoint = asWgs84Position(longitude, latitude, waypointDescription);
} else if ((segmentCount == 1) && (!waypointDescription.equals(positionPoint.getDescription()))) {
resultPoint = positionPoint;
resultPoint.setDescription(waypointDescription + ' ' + resultPoint.getDescription());
} else
resultPoint = positionPoint;
return resultPoint;
}
protected Wgs84Position readBlocktype_01(ByteBuffer byteBuffer, Wgs84Position positionPoint) throws EOFException {
/*
4 byte int 01 00 00 00
8 byte int Länge. diese 8 bytes nicht mitzählen
4 byte Textlänge
n byte Text. Wegpunktbeschreibung. PLZ,Ort,
evtl. hier zu ende
8 byte breite double
8 byte länge double
4 byte 0
+ unknown
*/
long blockLength = byteBuffer.getLong();
int startPosition = byteBuffer.position();
String waypointDescription = getText(byteBuffer);
double longitude = 0, latitude = 0;
if (byteBuffer.position() < startPosition + blockLength - 8) {
longitude = byteBuffer.getDouble();
latitude = byteBuffer.getDouble();
}
// skip the unknown bytes
byteBuffer.position((int) (startPosition + blockLength));
if (positionPoint == null)
return asWgs84Position(longitude, latitude, waypointDescription);
return positionPoint;
}
protected Wgs84Position readBlocktype_02(ByteBuffer byteBuffer, Wgs84Position positionPoint, int segmentCount) throws EOFException {
/*
4 byte int 01 00 00 00
8 byte int Länge. diese 8 bytes nicht mitzählen
4 byte Textlänge
n byte Text. Wegpunktbeschreibung. PLZ,Ort,
evtl. hier zu ende
4 byte 0
8 byte breite double
8 byte länge double
4 byte count following items??:
4 byte identifier
0x08: city ?
0x32: zip code ?
4 byte textlength
n byte Textlength
8 byte ?
*/
long blockLength = byteBuffer.getLong();
int startPosition = byteBuffer.position();
String waypointDescription = getText(byteBuffer);
//unknown 4 bytes
byteBuffer.getInt();
double longitude = 0, latitude = 0;
if (byteBuffer.position() < startPosition + blockLength - 8) {
longitude = byteBuffer.getDouble();
latitude = byteBuffer.getDouble();
}
// skip the additional information like city, zip
byteBuffer.position((int) (startPosition + blockLength));
Wgs84Position resultPoint;
if (positionPoint == null) {
resultPoint = asWgs84Position(longitude, latitude, waypointDescription);
} else if ((segmentCount == 1) && (!waypointDescription.equals(positionPoint.getDescription()))) {
resultPoint = positionPoint;
resultPoint.setDescription(waypointDescription + ' ' + resultPoint.getDescription());
} else
resultPoint = positionPoint;
return resultPoint;
}
protected Wgs84Position readBlocktype_04(ByteBuffer byteBuffer, Wgs84Position positionPoint, int segmentCount) throws EOFException {
/*
4 byte int 04 00 00 00
8 byte int als Länge diese nicht mitzählen
4 byte int Länge des Textes. Wenn 0, dann folgende trotzdem 4 Bytes.
n byte Text
manchmal hier schon zu Ende.
4 byte (int) bisher gefunden: 0, 1. manchmal auch nur 3 byte??
8 byte breite double
8 byte länge double
4 byte int bisher gefunden 2, 3,
4 byte int abhängig folgt bis zur Gesamtlänge - 8
0x0: 4 byte int dann:
00 7B A4 3F:
4 byte textlänge
n byte Text
05 00 00 00:
5 byte text
0x2: 8 byte ?
0x7:
0x9: 4 byte Textlänge
n byte Text
0x8: 4 byte Textlänge
n byte Text
0x32: es folgt
4 byte Textlänge
n byte PLZ
0x3C: es folgt
4 byte Textlänge
n byte Text
4 byte
4 byte Textlänge
n byte Text
4 byte Textlänge
n byte Text
8 byte ?
*/
long blockLength = byteBuffer.getLong();
int startPosition = byteBuffer.position();
int firstTextLen = byteBuffer.getInt();
String waypointDescription = getText(byteBuffer, firstTextLen);
// unknown 4 byte
if (byteBuffer.position() < startPosition + blockLength - 8) {
byteBuffer.getInt(); // 0 or 1
}
double longitude = 0;
double latitude = 0;
if (byteBuffer.position() < startPosition + blockLength - 8) {
longitude = byteBuffer.getDouble();
latitude = byteBuffer.getDouble();
// unknown 4 byte
byteBuffer.getInt();
}
while (byteBuffer.position() < startPosition + blockLength - 8) {
int dataType = byteBuffer.getInt();
switch (dataType) {
case 0x0:
if (byteBuffer.position() < startPosition + blockLength - 8) {
int textLen = byteBuffer.getInt();
if (textLen > START_BYTES)
textLen = byteBuffer.getInt();
getText(byteBuffer, textLen);
}
break;
case 0x2:
getText(byteBuffer); //firstname??
break;
case 0x5:
// unknown 5 bytes
for (int i = 0; i < 5; i++)
byteBuffer.get();
break;
case 0x8:
getText(byteBuffer);
break;
case 0x9:
getText(byteBuffer);
break;
case 0x32: //=50
getText(byteBuffer); //PLZ
break;
case 0x3C: //=60
getText(byteBuffer);
byteBuffer.getInt();
getText(byteBuffer);
getText(byteBuffer);
break;
default:
}
}
// 2x4 byte unknown
byteBuffer.getInt();
byteBuffer.getInt();
//Es gab eine Datei in der irgendein Fehler drin war. Damit konnte es
//passieren, dass über das Ziel hinaus gelesen wurde.
//Daher nie über das Ende hinaus. Oder sollte man gleich immer an das
//Ende gehen?
if (byteBuffer.position() > startPosition + blockLength)
byteBuffer.position((int) (startPosition + blockLength));
Wgs84Position resultPoint;
if (positionPoint == null) {
resultPoint = asWgs84Position(longitude, latitude, waypointDescription);
} else if ((segmentCount == 1) && (!waypointDescription.equals(positionPoint.getDescription()))) {
resultPoint = positionPoint;
resultPoint.setDescription(waypointDescription + ' ' + resultPoint.getDescription());
} else
resultPoint = positionPoint;
return resultPoint;
}
public void write(Wgs84Route route, PrintWriter writer, int startIndex, int endIndex) throws IOException {
throw new UnsupportedOperationException();
}
private byte[] encodePoint(Wgs84Position position, int positionNo, String mapName) throws UnsupportedEncodingException {
// Die Route besteht aus einem Punkt der mehrere weitere Unterpunktbeschreibungen hat.
// Im Navigongerät werden dort weitere Informationen wie übergeorgnete Stadt, Land, usw-
// gespeichert. Diese Informationen liegen nicht vor und werden daher auch nicht
// geschrieben.
// Nach nochmaliger Analyse (08.10.2011) mit einem von itconv geschriebenen .route
// scheint es notwendig zu sein, dass Land mit anzugeben.
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.order(LITTLE_ENDIAN);
byteBuffer.position(0);
byteBuffer.putInt(0); // bytelength of whole point will be filled at the end
byteBuffer.putLong(0); // 8 Byte 0
byteBuffer.putInt(0); // 4 Byte textlength
byteBuffer.putInt(1); // 4 Byte always 1
byteBuffer.putInt(2); // count following "02 00 00 00" Block.
int timeStamp = (int) (System.currentTimeMillis() / 1000L);
byte unknownBytes[] = { //copied from itconv export
(byte) 0x28, (byte) 0x00, (byte) 0x00, (byte) 0x00
};
//unix timestamp
byteBuffer.putInt(timeStamp);
byteBuffer.put(unknownBytes);
byteBuffer.putInt(4); // starttag
int positionStarttag = byteBuffer.position(); // save position to fill the bytelength at the end
byteBuffer.putLong(0); // length of following data. filled at the end
byte[] description = position.getDescription().getBytes(UTF8_ENCODING);
byteBuffer.putInt(description.length);
byteBuffer.put(description);
byteBuffer.putInt(0); //this 4 bytes only if startag = 4
byteBuffer.putDouble(position.getLongitude());
byteBuffer.putDouble(position.getLatitude());
byteBuffer.putInt(0); //4 byte ??
//0x08 ist freier Text. Liegt nicht vor -> mit 0 füllen
//macht itconv ebenso
byteBuffer.putInt(0x08);
byteBuffer.putLong(0); //8 byte
//unknown copyied from itconv export. wechselt in itconf an den ersten Stellen. Timestamp
//passt nicht. Datum ist von 1990
//Sind eigentlich 2x 4 Bytes. Die ersten 4 werden im Land nochmal verwendet
byte rawData[] = {
(byte) 0x90, (byte) 0xF9, (byte) 0x46, (byte) 0x27,
(byte) 0x0A, (byte) 0x00, (byte) 0x00, (byte) 0x00
};
rawData[0] += positionNo; //erhöht sich mit jedem Punkt
byteBuffer.put(rawData);
//Countrycode
//type 1
byteBuffer.putInt(4);
int positionStarttagCountry = byteBuffer.position(); // save position to fill the bytelength at the end
//bytelength
byteBuffer.putLong(0); //filled at the end
byteBuffer.putInt(mapName.length()); //textlänge
byteBuffer.put(mapName.getBytes(UTF8_ENCODING)); //3 bytes text
byteBuffer.putInt(timeStamp);
byteBuffer.put(rawData, 0, 4);
//20 Byte 0
byteBuffer.putLong(0);
byteBuffer.putLong(0);
byteBuffer.putInt(0);
int pointByteLength = byteBuffer.position();
// fill the bytelength fields
byteBuffer.putInt(0, pointByteLength - 4);
byteBuffer.putInt(positionStarttag, positionStarttagCountry - positionStarttag - 12);
byteBuffer.putInt(positionStarttagCountry, pointByteLength - positionStarttagCountry - 20 - 8);
byte[] result = new byte[pointByteLength];
byteBuffer.position(0);
byteBuffer.get(result);
return result;
}
private String calculateMapName(List<Wgs84Position> positions, int startIndex, int endIndex) {
String mapName = preferences.get("navigonRouteMapName", null);
if (mapName != null)
return mapName;
int westCount = 0;
for (int i = startIndex; i < endIndex; i++) {
Wgs84Position position = positions.get(i);
if (position.getLongitude() < -27.0)
westCount++;
}
int eastCount = endIndex - startIndex - westCount;
return westCount > eastCount ? "USA-CA" : "DEU";
}
public void write(Wgs84Route route, OutputStream target, int startIndex,
int endIndex) throws IOException {
// write all waypoints to buffer since we need at the end the size of all position bytes
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// 4 Byte always 0
byteArrayOutputStream.write(new byte[]{0, 0, 0, 0});
// 4 Byte Textlength Date - fix length
byteArrayOutputStream.write(new byte[]{0x12, 0, 0, 0});
SimpleDateFormat dateFormat = new SimpleDateFormat("'R'yyyyMMdd'-'HH:mm:ss");
String date = dateFormat.format(System.currentTimeMillis());
byteArrayOutputStream.write(date.getBytes(UTF8_ENCODING));
// 4 Byte Pointcount, max. 255 Points with this style
byteArrayOutputStream.write((byte) (endIndex - startIndex));
byteArrayOutputStream.write(new byte[]{0, 0, 0});
// 4 Byte int. Seen 0 and 1, currently writing always 1
byteArrayOutputStream.write(new byte[]{1, 0, 0, 0});
String mapName = calculateMapName(route.getPositions(), startIndex, endIndex);
int positionNo = 1;
for (int i = startIndex; i < endIndex; i++) {
Wgs84Position position = route.getPosition(i);
byte[] waypointBytes = encodePoint(position, positionNo++, mapName);
byteArrayOutputStream.write(waypointBytes);
}
byte[] header = new byte[16];
ByteBuffer headerBuffer = ByteBuffer.wrap(header);
headerBuffer.order(LITTLE_ENDIAN);
headerBuffer.position(0);
headerBuffer.putInt(START_BYTES);
headerBuffer.putLong(UNKNOWN_START_BYTES);
headerBuffer.putInt(byteArrayOutputStream.size() + 4);
target.write(header);
target.write(byteArrayOutputStream.toByteArray());
}
}