/*
* 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;
}
}
}