/*
* Copyright (C) 2016.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 or
* version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*/
package uk.me.parabola.mkgmap.combiners;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.imgfmt.fs.DirectoryEntry;
import uk.me.parabola.imgfmt.fs.FileSystem;
import uk.me.parabola.imgfmt.fs.ImgChannel;
import uk.me.parabola.imgfmt.sys.ImgFS;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.CommandArgs;
import static java.nio.file.StandardOpenOption.*;
/**
* Create a map in the gmapi format.
*
* This is directory tree containing an XML file describing the contents, and exploded versions of
* each .img file.
*/
public class GmapiBuilder
implements Combiner
{
private static final Logger log =
Logger.
getLogger(GmapiBuilder.
class);
private static final String NS =
"http://www.garmin.com/xmlschemas/MapProduct/v1";
private final Map<String, Combiner
> combinerMap
;
private final Map<String,
String> sourceMap
;
private Path gmapDir
;
private final Map<Integer, ProductInfo
> productMap =
new HashMap<>();
private String familyName
;
private int familyId
;
private short productVersion
;
private String typFile
;
private boolean forceWrite
;
private String mustWritePattern
;
public GmapiBuilder
(Map<String, Combiner
> combinerMap,
Map<String,
String> sourceMap
) {
this.
combinerMap = combinerMap
;
this.
sourceMap = sourceMap
;
}
/**
* Initialise with the command line arguments. This is called after all
* the command line arguments have been processed, but before any calls to
* the {@link #onMapEnd} methods.
*
* @param args The command line arguments.
*/
public void init
(CommandArgs args
) {
familyName = args.
get("family-name",
"OSM map");
familyId = args.
get("family-id", CommandArgs.
DEFAULT_FAMILYID);
productVersion =
(short) args.
get("product-version",
100);
gmapDir = Paths.
get(args.
getOutputDir(),
String.
format("%s.gmap", familyName
));
forceWrite = args.
exists("gmapi");
mustWritePattern = args.
get("gmapi-minimal",
null);
}
/**
* This is called when an individual map is complete.
*
* @param info An interface to read the map.
*/
public void onMapEnd
(FileInfo info
) {
String fn = info.
getFilename();
String mapname = info.
getMapname();
int productId = info.
getProductId();
if (!productMap.
containsKey(productId
))
productMap.
put(productId,
new ProductInfo
(productId, info.
getSeriesName(), info.
getOverviewName()));
// Unzip the image into the product tile directory.
try {
if (info.
isImg()) {
if (forceWrite || shouldWrite
(info
))
unzipImg
(fn, mapname, productId
);
}
else if (info.
getKind() == FileKind.
TYP_KIND)
typFile = info.
getFilename();
} catch (IOException e
) {
throw new ExitException
("Error saving gmapi data", e
);
}
}
private boolean shouldWrite
(FileInfo info
) {
String fn = info.
getFilename();
String source = sourceMap.
get(fn
);
if (!source.
equals(fn
)) {
log.
diagnostic("gmapi-minimal: Writing freshly compiled file " + fn
);
return true;
}
if (mustWritePattern
!=
null) {
if (fn.
matches(mustWritePattern
)) {
log.
diagnostic("gmapi-minimal: Writing old file " + fn +
" because it matches pattern " + mustWritePattern
);
return true;
}
}
log.
diagnostic("gmapi-minimal: Skipping file " + fn
);
return false;
}
/**
* The complete map set has been processed. Finish off anything that needs
* doing.
*/
public void onFinish
() {
try {
if (combinerMap.
containsKey("mdx")) {
File file =
new File(getFilenameFor
("mdx"));
Files.
copy(file.
toPath(), gmapDir.
resolve(file.
getName()), StandardCopyOption.
REPLACE_EXISTING);
}
if (combinerMap.
containsKey("mdr")) {
File file =
new File(getFilenameFor
("mdr"));
unzipImg
(file.
getCanonicalPath(), gmapDir.
resolve(nameWithoutExtension
(file
)));
}
if (typFile
!=
null) {
File file =
new File(typFile
);
Files.
copy(file.
toPath(), gmapDir.
resolve(file.
getName()), StandardCopyOption.
REPLACE_EXISTING);
}
for (ProductInfo info : productMap.
values()) {
finishTdbFile
(info
);
unzipImg
(getFilenameFor
("img"), info.
overviewName, info.
id);
}
writeXmlFile
(gmapDir
);
} catch (IOException e
) {
throw new ExitException
("Error building gmapi data", e
);
}
}
private static String nameWithoutExtension
(File file
) {
String name = file.
getName();
int len = name.
length();
if (len
< 4)
return name
;
return name.
substring(0, len-
4);
}
private void finishTdbFile
(ProductInfo info
) throws IOException {
Path tdbPath = Paths.
get(getFilenameFor
("tdb"));
Files.
copy(tdbPath, gmapDir
.
resolve(String.
format("Product%d", info.
id))
.
resolve(String.
format("%s.tdb", info.
overviewName)), StandardCopyOption.
REPLACE_EXISTING);
}
private void unzipImg
(String srcImgName,
String mapname,
int productId
) throws IOException {
Path destDir = Paths.
get(gmapDir.
toString(),
"Product" + productId, mapname
);
unzipImg
(srcImgName, destDir
);
}
private void unzipImg
(String srcImgName, Path destDir
) throws IOException {
FileSystem fs = ImgFS.
openFs(srcImgName
);
for (DirectoryEntry ent : fs.
list()) {
String fullname = ent.
getFullName();
try (ImgChannel f = fs.
open(fullname,
"r")) {
String name = displayName
(fullname
);
if (Objects.
equals(name,
"."))
continue;
Files.
createDirectories(destDir
);
Path out = destDir.
resolve(name
);
copyToFile
(f, out
);
}
}
}
private static void copyToFile
(ImgChannel f, Path dest
) {
ByteBuffer buf =
ByteBuffer.
allocate(8 * 1024);
try (ByteChannel outchan = Files.
newByteChannel(dest, CREATE, WRITE, TRUNCATE_EXISTING
)) {
while (f.
read(buf
) > 0) {
buf.
flip();
outchan.
write(buf
);
buf.
compact();
}
} catch (IOException e
) {
throw new ExitException
("Cannot write file " + e
);
}
}
private String getFilenameFor
(String kind
) {
return combinerMap.
get(kind
).
getFilename();
}
private static String displayName
(String fullname
) {
return fullname.
trim().
replace("\000",
"");
}
/**
* An xml file contains similar information that is contained in the windows registry.
*
* @param gDir The directory where the Info.xml file will be created.
*/
private void writeXmlFile
(Path gDir
) {
Path infoFile = gDir.
resolve("Info.xml");
XMLOutputFactory factory = XMLOutputFactory.
newFactory();
try (Writer stream = Files.
newBufferedWriter(infoFile
)) {
XMLStreamWriter writer = factory.
createXMLStreamWriter(stream
);
writer.
writeStartDocument("UTF-8",
"1.0");
writer.
setDefaultNamespace(NS
);
writer.
writeCharacters("\n");
writer.
writeStartElement(NS,
"MapProduct");
writer.
writeDefaultNamespace(NS
);
writer.
writeCharacters("\n");
xmlElement
(writer,
"Name", familyName
);
xmlElement
(writer,
"DataVersion",
String.
valueOf(productVersion
));
xmlElement
(writer,
"DataFormat",
"Original");
xmlElement
(writer,
"ID",
String.
valueOf(familyId
));
if (combinerMap.
containsKey("mdx")) {
String mdxFile = getFilenameFor
("mdx");
File file =
new File(mdxFile
);
xmlElement
(writer,
"IDX", file.
getName());
}
if (combinerMap.
containsKey("mdr")) {
String mdrName = getFilenameFor
("mdr");
File file =
new File(mdrName
);
xmlElement
(writer,
"MDR", nameWithoutExtension
(file
));
}
if (typFile
!=
null) {
File file =
new File(typFile
);
xmlElement
(writer,
"TYP", file.
getName());
}
for (ProductInfo prod : productMap.
values()) {
writer.
writeStartElement(NS,
"SubProduct");
writer.
writeCharacters("\n");
xmlElement
(writer,
"Name", prod.
seriesName);
xmlElement
(writer,
"ID",
String.
valueOf(prod.
id));
xmlElement
(writer,
"BaseMap", prod.
overviewName);
xmlElement
(writer,
"TDB",
String.
format("%s.tdb", prod.
overviewName));
xmlElement
(writer,
"Directory",
String.
format("Product%s", prod.
id));
writer.
writeEndElement();
writer.
writeCharacters("\n");
}
writer.
writeEndElement();
writer.
writeEndDocument();
writer.
flush();
} catch (XMLStreamException |
IOException e
) {
throw new ExitException
("Could not create file " + infoFile +
"; " + e
);
}
}
private static void xmlElement
(XMLStreamWriter writer,
String name,
String value
) throws XMLStreamException
{
writer.
writeCharacters(" ");
writer.
writeStartElement(NS, name
);
writer.
writeCharacters(value
);
writer.
writeEndElement();
writer.
writeCharacters("\n");
}
private static class ProductInfo
{
private final String seriesName
;
private final String overviewName
;
private final int id
;
public ProductInfo
(int id,
String seriesName,
String overviewName
) {
this.
id = id
;
this.
seriesName = seriesName
;
this.
overviewName = overviewName
;
}
}
}