/* * 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.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 final static String NS = "http://www.garmin.com/xmlschemas/MapProduct/v1"; private final Map<String, Combiner> combinerMap; private Path gmapDir; private final Map<Integer, ProductInfo> productMap = new HashMap<>(); private String familyName; private int familyId; private short productVersion; private String typFile; public GmapiBuilder(Map<String, Combiner> combinerMap) { this.combinerMap = combinerMap; } /** * 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)); } /** * 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()) unzipImg(fn, mapname, productId); else if (info.getKind() == FileKind.TYP_KIND) typFile = info.getFilename(); } catch (IOException e) { e.printStackTrace(); } } /** * 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) { e.printStackTrace(); } } private 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); copyToFile(f, destDir.resolve(name)); } } } private 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 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 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; } } }