/* * Copyright (C) 2007 Steve Ratcliffe * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License 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. * * * Author: Steve Ratcliffe * Create date: Nov 15, 2007 */ package uk.me.parabola.mkgmap.combiners; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import uk.me.parabola.imgfmt.FileExistsException; import uk.me.parabola.imgfmt.FileNotWritableException; import uk.me.parabola.imgfmt.FileSystemParam; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.app.srt.SRTFile; import uk.me.parabola.imgfmt.app.srt.Sort; 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.mps.MapBlock; import uk.me.parabola.imgfmt.mps.MpsFile; import uk.me.parabola.imgfmt.mps.MpsFileReader; import uk.me.parabola.imgfmt.mps.ProductBlock; import uk.me.parabola.imgfmt.sys.FileImgChannel; import uk.me.parabola.imgfmt.sys.ImgFS; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.CommandArgs; /** * Create the gmapsupp file. There is nothing much special about this file * (as far as I know - there's not a public official spec or anything) it is * just a regular .img file which is why it works to rename a single .img file * and send it to the device. * <p/> * Effectively we just 'unzip' the constituent .img files and then 'zip' them * back into the gmapsupp.img file. * <p/> * In addition we need to create and add the MPS file, if we don't already * have one. * * @author Steve Ratcliffe */ public class GmapsuppBuilder implements Combiner { private static final Logger log = Logger.getLogger(GmapsuppBuilder.class); private static final String GMAPSUPP = "gmapsupp.img"; /** * The number of block numbers that will fit into one entry block */ private static final int ENTRY_SIZE = 240; private static final int DIRECTORY_OFFSET_ENTRY = 2; private final Map<String, FileInfo> files = new LinkedHashMap<>(); // all these need to be set in the init routine from arguments. private String areaName; private String mapsetName; private String overallDescription = "Combined map"; private String outputDir; private MpsFile mpsFile; private boolean createIndex; // True if we should create and add an index file // There is a separate MDR and SRT file for each family id in the gmapsupp private final Map<Integer, MdrBuilder> mdrBuilderMap = new LinkedHashMap<>(); private final Map<Integer, Sort> sortMap = new LinkedHashMap<>(); private boolean splitName; private boolean hideGmapsuppOnPC; public void init(CommandArgs args) { areaName = args.get("area-name", null); mapsetName = args.get("mapset-name", "OSM map set"); overallDescription = args.getDescription(); outputDir = args.getOutputDir(); splitName = args.get("split-name-index", false); hideGmapsuppOnPC = args.get("hide-gmapsupp-on-pc", false); } /** * Add or retrieve the MDR file for the given familyId. * @param familyId The family id to create the mdr file for. * @param sort The sort for this family id. * @param outputDir The place to write the file. * @return If there is already an mdr file for this family then it is returned, else the newly created * one. */ private MdrBuilder addMdrFile(int familyId, Sort sort, String outputDir) { MdrBuilder mdrBuilder = mdrBuilderMap.get(familyId); if (mdrBuilder != null) return mdrBuilder; mdrBuilder = new MdrBuilder(); mdrBuilder.initForDevice(sort, outputDir, splitName); mdrBuilderMap.put(familyId, mdrBuilder); return mdrBuilder; } /** * Add the sort file for the given family id. */ private void addSrtFile(int familyId, FileInfo info) { Sort prevSort = sortMap.get(familyId); Sort sort = info.getSort(); if (prevSort == null) { if (info.getKind() == FileKind.IMG_KIND) { sortMap.put(familyId, sort); } } else { if (prevSort.getCodepage() != sort.getCodepage()) System.err.printf("WARNING: input file '%s' has a different code page (%d rather than %d)\n", info.getFilename(), sort.getCodepage(), prevSort.getCodepage()); if (info.hasSortOrder() && prevSort.getSortOrderId() != sort.getSortOrderId()) System.err.printf("WARNING: input file '%s' has a different sort order (%x rather than %x\n", info.getFilename(), sort.getSortOrderId(), prevSort.getSortOrderId()); } } /** * This is called when the map is complete. We collect information about the map to be used in the TDB file and for * preparing the gmapsupp file. * * @param info Information about the img file. */ public void onMapEnd(FileInfo info) { files.put(info.getFilename(), info); if (info.isImg()) { int familyId = info.getFamilyId(); if (createIndex) { MdrBuilder mdrBuilder = addMdrFile(familyId, info.getSort(), info.getOutputDir()); mdrBuilder.onMapEnd(info); } addSrtFile(familyId, info); } } /** * The complete map set has been processed. Creates the gmapsupp file. This is done by stepping through each img file, * reading all the sub files and copying them into the gmapsupp file. */ public void onFinish() { for (MdrBuilder mdrBuilder : mdrBuilderMap.values()) { mdrBuilder.onFinishForDevice(); } FileSystem imgFs = null; try { imgFs = createGmapsupp(); addAllFiles(imgFs); // Add all the MDR files (one for each family) for (Map.Entry<Integer, MdrBuilder> ent : mdrBuilderMap.entrySet()) addFile(imgFs, ent.getValue().getFileName(), String.format("%08d.MDR", ent.getKey())); writeSrtFile(imgFs); writeMpsFile(); } catch (FileNotWritableException e) { log.warn("Could not create gmapsupp file"); System.err.println("Could not create gmapsupp file"); } finally { Utils.closeFile(imgFs); } } /** * Write the SRT file. * * @param imgFs The filesystem to create the SRT file in. * @throws FileNotWritableException If it cannot be created. */ private void writeSrtFile(FileSystem imgFs) throws FileNotWritableException { for (Map.Entry<Integer, Sort> ent : sortMap.entrySet()) { Sort sort = ent.getValue(); int familyId = ent.getKey(); if (sort.getId1() == 0 && sort.getId2() == 0) return; ImgChannel channel = null; try { channel = imgFs.create(String.format("%08d.SRT", familyId)); SRTFile srtFile = new SRTFile(channel); srtFile.setSort(sort); srtFile.write(); srtFile.close(); } catch (FileExistsException e) { // well it shouldn't exist! log.error("could not create SRT file as it exists already"); throw new FileNotWritableException("already existed", e); } finally { Utils.closeFile(channel); } } } /** * Write the MPS file. The gmapsupp file will work without this, but it important if you want to include more than one * map family and be able to turn them on and off separately. */ private void writeMpsFile() throws FileNotWritableException { try { mpsFile.sync(); mpsFile.close(); } catch (IOException e) { throw new FileNotWritableException("Could not finish write to MPS file", e); } } private MapBlock makeMapBlock(FileInfo info) { MapBlock mb = new MapBlock(info.getCodePage()); mb.setMapNumber(info.getMapnameAsInt()); mb.setHexNumber(info.getHexname()); mb.setMapDescription(info.getDescription()); mb.setAreaName(areaName != null ? areaName : "Area " + info.getMapname()); mb.setSeriesName(info.getSeriesName()); mb.setIds(info.getFamilyId(), info.getProductId()); return mb; } private ProductBlock makeProductBlock(FileInfo info) { ProductBlock pb = new ProductBlock(info.getCodePage()); pb.setFamilyId(info.getFamilyId()); pb.setProductId(info.getProductId()); pb.setDescription(info.getFamilyName()); return pb; } private void addAllFiles(FileSystem outfs) { for (FileInfo info : files.values()) { String filename = info.getFilename(); switch (info.getKind()) { case IMG_KIND: addImg(outfs, filename); addMpsEntry(info); break; case GMAPSUPP_KIND: addImg(outfs, filename); addMpsFile(info); break; case APP_KIND: case TYP_KIND: addFile(outfs, filename); break; case MDR_KIND: break; } } } /** * Add a complete pre-existing mps file to the mps file we are currently * building for this gmapsupp. * @param info The details of the gmapsupp file that we need to extract the */ private void addMpsFile(FileInfo info) { String name = info.getFilename(); FileSystem fs = null; try { fs = ImgFS.openFs(name); MpsFileReader mr = new MpsFileReader(fs.open(info.getMpsName(), "r"), info.getCodePage()); for (MapBlock block : mr.getMaps()) mpsFile.addMap(block); for (ProductBlock b : mr.getProducts()) mpsFile.addProduct(b); mr.close(); } catch (IOException e) { log.error("Could not read MPS file from gmapsupp", e); } finally { Utils.closeFile(fs); } } /** * Add a single entry to the mps file. * @param info The img file information. */ private void addMpsEntry(FileInfo info) { mpsFile.addMap(makeMapBlock(info)); // Add a new product block if we have found a new product mpsFile.addProduct(makeProductBlock(info)); } private MpsFile createMpsFile(FileSystem outfs) throws FileNotWritableException { try { ImgChannel channel = outfs.create("MAKEGMAP.MPS"); return new MpsFile(channel); } catch (FileExistsException e) { // well it shouldn't exist! log.error("could not create MPS file as it already exists"); throw new FileNotWritableException("already existed", e); } } /** * Add a single file to the output. * * @param outfs The output gmapsupp file. * @param filename The input filename. */ private void addFile(FileSystem outfs, String filename) { String imgname = createImgFilename(filename); addFile(outfs, filename, imgname); } private void addFile(FileSystem outfs, String filename, String imgname) { ImgChannel chan = new FileImgChannel(filename, "r"); try { copyFile(chan, outfs, imgname); } catch (IOException e) { log.error("Could not write file " + filename); } } /** * Create a suitable filename for use in the .img file from the external * file name. * * The external file name might look something like /home/steve/foo.typ * or c:\maps\foo.typ and we need to take the filename part and make * sure that it is no more than 8+3 characters. * * @param pathname The external filesystem path name. * @return The filename part, will be restricted to 8+3 characters and all * in upper case. */ private String createImgFilename(String pathname) { File f = new File(pathname); String name = f.getName().toUpperCase(Locale.ENGLISH); int dot = name.lastIndexOf('.'); String base = name.substring(0, dot); if (base.length() > 8) base = base.substring(0, 8); String ext = name.substring(dot + 1); if (ext.length() > 3) ext = ext.substring(0, 3); return base + '.' + ext; } /** * Add a complete .img file, that is all the constituent files from it. * * @param outfs The gmapsupp file to write to. * @param filename The input filename. */ private void addImg(FileSystem outfs, String filename) { try { try (FileSystem infs = ImgFS.openFs(filename)) { copyAllFiles(infs, outfs); } } catch (FileNotFoundException e) { log.error("Could not open file " + filename); } } /** * Copy all files from the input filesystem to the output filesystem. * * @param infs The input filesystem. * @param outfs The output filesystem. */ private void copyAllFiles(FileSystem infs, FileSystem outfs) { List<DirectoryEntry> entries = infs.list(); for (DirectoryEntry ent : entries) { String ext = ent.getExt(); if (ext.equals(" ") || ext.equals("MPS")) continue; String inname = ent.getFullName(); try { copyFile(inname, infs, outfs); } catch (IOException e) { log.warn("Could not copy " + inname, e); } } } /** * Create the output file. * * @return The gmapsupp file. * @throws FileNotWritableException If it cannot be created for any reason. */ private FileSystem createGmapsupp() throws FileNotWritableException { BlockInfo bi = calcBlockSize(); int blockSize = bi.blockSize; // Create this file, containing all the sub files FileSystemParam params = new FileSystemParam(); params.setBlockSize(blockSize); params.setMapDescription(overallDescription); params.setDirectoryStartEntry(DIRECTORY_OFFSET_ENTRY); params.setGmapsupp(true); params.setHideGmapsuppOnPC(hideGmapsuppOnPC); int reserveBlocks = (int) Math.ceil(bi.reserveEntries * 512.0 / blockSize); params.setReservedDirectoryBlocks(reserveBlocks); FileSystem outfs = ImgFS.createFs(Utils.joinPath(outputDir, GMAPSUPP), params); mpsFile = createMpsFile(outfs); mpsFile.setMapsetName(mapsetName); return outfs; } /** * Copy an individual file with the given name from the first archive/filesystem * to the second. * * @param inName The name of the file. * @param infs The filesystem to copy from. * @param outfs The filesystem to copy to. * @throws IOException If the copy fails. */ private void copyFile(String inName, FileSystem infs, FileSystem outfs) throws IOException { ImgChannel fin = infs.open(inName, "r"); copyFile(fin, outfs, inName); } /** * Copy a given open file to the a new file in outfs with the name inName. * @param fin The file to copy from. * @param outfs The file system to copy to. * @param inName The name of the file to create on the destination file system. * @throws IOException If a file cannot be read or written. */ private void copyFile(ImgChannel fin, FileSystem outfs, String inName) throws IOException { ImgChannel fout = outfs.create(inName); copyFile(fin, fout); } /** * Copy an individual file with the given name from the first archive/filesystem * to the second. * * @param fin The file to copy from. * @param fout The file to copy to. * @throws IOException If the copy fails. */ private void copyFile(ImgChannel fin, ImgChannel fout) throws IOException { try { ByteBuffer buf = ByteBuffer.allocate(1024); while (fin.read(buf) > 0) { buf.flip(); fout.write(buf); buf.compact(); } } finally { fin.close(); fout.close(); } } /** * Calculate the block size that we need to use. The block size must be such that * the total number of blocks is less than 0xffff. * * I am making sure that the that the root directory entry doesn't require * more than one block to hold its own block list. * * @return A suitable block size to use for the gmapsupp.img file. */ private BlockInfo calcBlockSize() { int[] ints = {1 << 9, 1 << 10, 1 << 11, 1 << 12, 1 << 13, 1 << 14, 1 << 15, 1 << 16, 1 << 17, 1 << 18, 1 << 19, 1 << 20, 1 << 21, 1 << 22, 1 << 23, 1 << 24, }; for (int bs : ints) { int totBlocks = 0; int totHeaderEntries = 0; for (FileInfo info : files.values()) { totBlocks += info.getNumBlocks(bs); // Each file will take up at least one directory block. // Each directory block can hold 480 block-references int slots = info.getNumHeaderEntries(bs); log.info("adding", slots, "slots for", info.getFilename()); totHeaderEntries += slots; } // Estimate the number of blocks needed for the MPS file int mpsSize = files.size() * 80 + 100; int mpsBlocks = (mpsSize + (bs - 1)) / bs; int mpsSlots = (mpsBlocks + ENTRY_SIZE - 1) / ENTRY_SIZE; totBlocks += mpsBlocks; totHeaderEntries += mpsSlots; // Add in number of block for mdr if (createIndex) { for (MdrBuilder mdrBuilder : mdrBuilderMap.values()) { int sz = mdrBuilder.getSize(); int mdrBlocks = (sz + (bs - 1)) / bs; int mdrSlots = (mdrBlocks + ENTRY_SIZE - 1) / ENTRY_SIZE; totBlocks += mdrBlocks; totHeaderEntries += mdrSlots; } } for (int i = 0; i < sortMap.size(); i++) { // These files are less than 1k int sz = 1024; int mdrBlocks = (sz + (bs - 1)) / bs; int mdrSlots = (mdrBlocks + ENTRY_SIZE - 1) / ENTRY_SIZE; totBlocks += mdrBlocks; totHeaderEntries += mdrSlots; } // Add for header itself, plus the first directory block. totHeaderEntries += DIRECTORY_OFFSET_ENTRY + 1; int totHeaderBlocks = totHeaderEntries * 512 / bs; log.info("total blocks for", bs, "is", totHeaderBlocks, "based on slots=", totHeaderEntries); if (totBlocks + totHeaderEntries < 0xfffe && totHeaderBlocks <= ENTRY_SIZE) { return new BlockInfo(bs, totHeaderEntries); } } throw new IllegalArgumentException("Could not select a suitable block size. Try to reduce the number of splits."); } public void setCreateIndex(boolean create) { this.createIndex = create; } /** * Just a data value object for various bits of block size info. */ private static class BlockInfo { private final int blockSize; private final int reserveEntries; private BlockInfo(int blockSize, int reserveEntries) { this.blockSize = blockSize; this.reserveEntries = reserveEntries; } } }