/*
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.base;
import slash.common.io.NotClosingUnderlyingInputStream;
import slash.common.type.CompactCalendar;
import slash.navigation.babel.BabelFormat;
import slash.navigation.bcr.BcrFormat;
import slash.navigation.copilot.CoPilotFormat;
import slash.navigation.gpx.Gpx11Format;
import slash.navigation.gpx.GpxFormat;
import slash.navigation.itn.TomTomRouteFormat;
import slash.navigation.kml.Kml22Format;
import slash.navigation.nmn.NmnFormat;
import slash.navigation.tcx.TcxFormat;
import slash.navigation.url.GoogleMapsUrlFormat;
import slash.navigation.url.MotoPlanerUrlFormat;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
import static java.io.File.separatorChar;
import static java.lang.Math.min;
import static java.lang.String.format;
import static slash.common.io.Transfer.ceiling;
import static slash.common.type.CompactCalendar.UTC;
import static slash.common.type.CompactCalendar.fromCalendar;
import static slash.navigation.base.NavigationFormats.asFormat;
import static slash.navigation.base.NavigationFormats.asFormatForRoutes;
import static slash.navigation.base.NavigationFormats.getReadFormats;
import static slash.navigation.base.RouteComments.commentPositions;
import static slash.navigation.base.RouteComments.commentRouteName;
import static slash.navigation.base.RouteComments.commentRoutePositions;
import static slash.navigation.base.RouteComments.createRouteName;
import static slash.navigation.url.GoogleMapsUrlFormat.isGoogleMapsLinkUrl;
import static slash.navigation.url.GoogleMapsUrlFormat.isGoogleMapsProfileUrl;
import static slash.navigation.url.MotoPlanerUrlFormat.isMotoPlanerUrl;
/**
* Parses byte streams with navigation information via {@link NavigationFormat} classes.
*
* @author Christian Pesch
*/
public class NavigationFormatParser {
private static final Logger log = Logger.getLogger(NavigationFormatParser.class.getName());
private static final int READ_BUFFER_SIZE = 1024 * 1024;
private final List<NavigationFormatParserListener> listeners = new CopyOnWriteArrayList<NavigationFormatParserListener>();
public void addNavigationFileParserListener(NavigationFormatParserListener listener) {
listeners.add(listener);
}
public void removeNavigationFileParserListener(NavigationFormatParserListener listener) {
listeners.remove(listener);
}
private void notifyReading(NavigationFormat<BaseRoute> format) {
for (NavigationFormatParserListener listener : listeners) {
listener.reading(format);
}
}
private List<Integer> getPositionCounts(List<BaseRoute> routes) {
List<Integer> positionCounts = new ArrayList<Integer>();
for (BaseRoute route : routes)
positionCounts.add(route.getPositionCount());
return positionCounts;
}
@SuppressWarnings("unchecked")
private void internalRead(InputStream buffer, CompactCalendar startDate,
List<NavigationFormat> formats, ParserContext context) throws IOException {
int routeCountBefore = context.getRoutes().size();
try {
for (NavigationFormat<BaseRoute> format : formats) {
notifyReading(format);
log.fine(format("Trying to read with %s", format));
try {
format.read(buffer, startDate, context);
} catch (Exception e) {
log.severe(format("Error reading with %s: %s, %s", format, e.getClass(), e));
}
if (context.getRoutes().size() > routeCountBefore) {
context.addFormat(format);
break;
}
try {
buffer.reset();
} catch (IOException e) {
log.severe("Cannot reset() stream to mark()");
break;
}
}
} finally {
buffer.close();
}
}
public ParserResult read(File source, List<NavigationFormat> formats) throws IOException {
log.info("Reading '" + source.getAbsolutePath() + "' by " + formats.size() + " formats");
FileInputStream fis = new FileInputStream(source);
NotClosingUnderlyingInputStream buffer = new NotClosingUnderlyingInputStream(new BufferedInputStream(fis));
buffer.mark((int) source.length() + 1);
try {
return read(buffer, (int) source.length(), getStartDate(source), formats);
} finally {
buffer.closeUnderlyingInputStream();
}
}
public ParserResult read(File source) throws IOException {
return read(source, getReadFormats());
}
private NavigationFormat determineFormat(List<BaseRoute> routes, NavigationFormat preferredFormat) {
NavigationFormat result = preferredFormat;
for (BaseRoute route : routes) {
// more than one route: the same result
if (result.equals(route.getFormat()))
continue;
// result is capable of storing multiple routes
if (result.isSupportsMultipleRoutes())
continue;
// result from GPSBabel-based format which allows only one route but is represented by GPX 1.0
if (result instanceof BabelFormat)
continue;
// default for multiple routes is GPX 1.1
result = new Gpx11Format();
}
return result;
}
@SuppressWarnings("unchecked")
private void commentRoutes(List<BaseRoute> routes) {
commentRoutePositions(routes);
for (BaseRoute<BaseNavigationPosition, BaseNavigationFormat> route : routes) {
commentRouteName(route);
}
}
@SuppressWarnings("unchecked")
private void commentRoute(BaseRoute route) {
commentPositions(route.getPositions());
commentRouteName(route);
}
@SuppressWarnings("unchecked")
private ParserResult createResult(ParserContext<BaseRoute> context) throws IOException {
List<BaseRoute> source = context.getRoutes();
if (source != null && source.size() > 0) {
NavigationFormat format = determineFormat(source, context.getFormats().get(0));
List<BaseRoute> destination = asFormatForRoutes(source, format);
log.info("Detected '" + format.getName() + "' with " + destination.size() + " route(s) and " +
getPositionCounts(destination) + " positions");
commentRoutes(destination);
return new ParserResult(new FormatAndRoutes(format, destination));
} else
return new ParserResult(null);
}
private class InternalParserContext<R extends BaseRoute> extends ParserContextImpl<R> {
public void parse(InputStream inputStream, CompactCalendar startDate, List<NavigationFormat> formats) throws IOException {
internalRead(inputStream, startDate, formats, this);
}
public void parse(String urlString) throws IOException {
// replace CWD with current working directory for easier testing
urlString = urlString.replace("CWD", new File(".").getCanonicalPath()).replace(separatorChar, '/');
URL url = new URL(urlString);
int readBufferSize = getSize(url);
log.info("Reading '" + url + "' with a buffer of " + readBufferSize + " bytes");
NotClosingUnderlyingInputStream buffer = new NotClosingUnderlyingInputStream(new BufferedInputStream(url.openStream()));
buffer.mark(readBufferSize + 1);
try {
internalRead(buffer, getStartDate(url), getReadFormats(), this);
} finally {
buffer.closeUnderlyingInputStream();
}
}
}
private ParserResult read(InputStream source, int readBufferSize, CompactCalendar startDate,
List<NavigationFormat> formats) throws IOException {
log.fine("Reading '" + source + "' with a buffer of " + readBufferSize + " bytes by " + formats.size() + " formats");
NotClosingUnderlyingInputStream buffer = new NotClosingUnderlyingInputStream(new BufferedInputStream(source));
buffer.mark(readBufferSize + 1);
try {
ParserContext<BaseRoute> context = new InternalParserContext<BaseRoute>();
internalRead(buffer, startDate, formats, context);
return createResult(context);
} finally {
buffer.closeUnderlyingInputStream();
}
}
public ParserResult read(String source) throws IOException {
return read(new ByteArrayInputStream(source.getBytes()));
}
public ParserResult read(InputStream source) throws IOException {
return read(source, READ_BUFFER_SIZE, null, getReadFormats());
}
public ParserResult read(InputStream source, List<NavigationFormat> formats) throws IOException {
return read(source, READ_BUFFER_SIZE, null, formats);
}
private int getSize(URL url) throws IOException {
try {
if (url.getProtocol().equals("file"))
return (int) new File(url.toURI()).length();
else
return READ_BUFFER_SIZE;
} catch (URISyntaxException e) {
throw new IOException("Cannot determine file from URL: " + e);
}
}
private CompactCalendar getStartDate(File file) {
Calendar startDate = Calendar.getInstance(UTC);
startDate.setTimeInMillis(file.lastModified());
return fromCalendar(startDate);
}
private CompactCalendar getStartDate(URL url) throws IOException {
try {
if (url.getProtocol().equals("file")) {
return getStartDate(new File(url.toURI()));
} else
return null;
} catch (URISyntaxException e) {
throw new IOException("Cannot determine file from URL: " + e);
}
}
public ParserResult read(URL url, List<NavigationFormat> formats) throws IOException {
if (isGoogleMapsProfileUrl(url)) {
url = new URL(url.toExternalForm() + "&output=kml");
formats = new ArrayList<NavigationFormat>(formats);
formats.add(0, new Kml22Format());
} else if (isGoogleMapsLinkUrl(url)) {
byte[] bytes = url.toExternalForm().getBytes();
List<NavigationFormat> readFormats = new ArrayList<NavigationFormat>(formats);
readFormats.add(0, new GoogleMapsUrlFormat());
return read(new ByteArrayInputStream(bytes), bytes.length, null, readFormats);
} else if (isMotoPlanerUrl(url)) {
byte[] bytes = url.toExternalForm().getBytes();
List<NavigationFormat> readFormats = new ArrayList<NavigationFormat>(formats);
readFormats.add(0, new MotoPlanerUrlFormat());
return read(new ByteArrayInputStream(bytes), bytes.length, null, readFormats);
}
int readBufferSize = getSize(url);
log.info("Reading '" + url + "' with a buffer of " + readBufferSize + " bytes");
return read(url.openStream(), readBufferSize, getStartDate(url), formats);
}
public static int getNumberOfFilesToWriteFor(BaseRoute route, NavigationFormat format, boolean duplicateFirstPosition) {
return ceiling(route.getPositionCount() + (duplicateFirstPosition ? 1 : 0), format.getMaximumPositionCount(), true);
}
@SuppressWarnings("unchecked")
private void write(BaseRoute route, NavigationFormat format,
boolean duplicateFirstPosition,
boolean ignoreMaximumPositionCount,
ParserCallback parserCallback,
OutputStream... targets) throws IOException {
log.info("Writing '" + format.getName() + "' position lists with 1 route and " + route.getPositionCount() + " positions");
BaseRoute routeToWrite = asFormat(route, format);
commentRoute(routeToWrite);
preprocessRoute(routeToWrite, format, duplicateFirstPosition, parserCallback);
int positionsToWrite = routeToWrite.getPositionCount();
int writeInOneChunk = format.getMaximumPositionCount();
// check if the positions to write fit within the given files
if (positionsToWrite > targets.length * writeInOneChunk) {
if (ignoreMaximumPositionCount)
writeInOneChunk = positionsToWrite;
else
throw new IOException("Found " + positionsToWrite + " positions, " + format.getName() +
" format may only contain " + writeInOneChunk + " positions in one position list.");
}
int startIndex = 0;
for (int i = 0; i < targets.length; i++) {
OutputStream target = targets[i];
int endIndex = min(startIndex + writeInOneChunk, positionsToWrite);
renameRoute(route, routeToWrite, startIndex, endIndex, i, targets);
format.write(routeToWrite, target, startIndex, endIndex);
log.info("Wrote position list from " + startIndex + " to " + endIndex);
startIndex += writeInOneChunk;
}
postProcessRoute(routeToWrite, format, duplicateFirstPosition);
}
public void write(BaseRoute route, NavigationFormat format,
boolean duplicateFirstPosition,
boolean ignoreMaximumPositionCount,
ParserCallback parserCallback,
File... targets) throws IOException {
OutputStream[] targetStreams = new OutputStream[targets.length];
for (int i = 0; i < targetStreams.length; i++)
targetStreams[i] = new FileOutputStream(targets[i]);
write(route, format, duplicateFirstPosition, ignoreMaximumPositionCount, parserCallback, targetStreams);
for (File target : targets)
log.info("Wrote '" + target.getAbsolutePath() + "'");
}
@SuppressWarnings("unchecked")
private void preprocessRoute(BaseRoute routeToWrite, NavigationFormat format,
boolean duplicateFirstPosition,
ParserCallback parserCallback) {
if (format instanceof NmnFormat)
routeToWrite.removeDuplicates();
if (format instanceof NmnFormat && duplicateFirstPosition)
routeToWrite.add(0, ((NmnFormat) format).getDuplicateFirstPosition(routeToWrite));
if (format instanceof CoPilotFormat && duplicateFirstPosition)
routeToWrite.add(0, ((CoPilotFormat) format).getDuplicateFirstPosition(routeToWrite));
if (format instanceof TcxFormat)
routeToWrite.ensureIncreasingTime();
if (parserCallback != null)
parserCallback.preprocess(routeToWrite, format);
}
@SuppressWarnings("unchecked")
private void renameRoute(BaseRoute route, BaseRoute routeToWrite, int startIndex, int endIndex, int trackIndex, OutputStream... targets) {
// gives splitted TomTomRoute and SimpleRoute routes a more useful name for the fragment
if (route.getFormat() instanceof TomTomRouteFormat || route.getFormat() instanceof SimpleFormat ||
route.getFormat() instanceof GpxFormat && routeToWrite.getFormat() instanceof BcrFormat) {
String name = createRouteName(routeToWrite.getPositions().subList(startIndex, endIndex));
if (targets.length > 1)
name = "Track" + (trackIndex + 1) + ": " + name;
routeToWrite.setName(name);
}
}
private void postProcessRoute(BaseRoute routeToWrite, NavigationFormat format, boolean duplicateFirstPosition) {
if ((format instanceof NmnFormat || format instanceof CoPilotFormat) && duplicateFirstPosition)
routeToWrite.remove(0);
}
@SuppressWarnings("unchecked")
public void write(List<BaseRoute> routes, MultipleRoutesFormat format, File target) throws IOException {
log.info("Writing '" + format.getName() + "' with with " + routes.size() + " routes and " +
getPositionCounts(routes) + " positions");
List<BaseRoute> routesToWrite = new ArrayList<BaseRoute>(routes.size());
for (BaseRoute route : routes) {
BaseRoute routeToWrite = asFormat(route, format);
commentRoute(routeToWrite);
preprocessRoute(routeToWrite, format, false, null);
routesToWrite.add(routeToWrite);
postProcessRoute(routeToWrite, format, false);
}
format.write(routesToWrite, new FileOutputStream(target));
log.info("Wrote '" + target.getAbsolutePath() + "'");
}
}