Subversion Repositories mkgmap

Rev

Rev 3749 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * 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;
        private int productVersion;


        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);
                productVersion = args.get("product-version", 100);
        }

        /**
         * 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);
                params.setProductVersion(productVersion);

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