Subversion Repositories mkgmap

Rev

Rev 4509 | 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.Closeable;
import java.io.File;
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.MapFailedException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.mdr.MdrConfig;
import uk.me.parabola.imgfmt.app.srt.SRTFile;
import uk.me.parabola.imgfmt.app.srt.Sort;
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.FileLink;
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 String GMAPSUPP = "gmapsupp.img";

        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 MdrConfig mdrConfig; // one base config for all
        private boolean hideGmapsuppOnPC;
        private int productVersion;

        private FileSystem imgFs;

        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();
                hideGmapsuppOnPC = args.get("hide-gmapsupp-on-pc", false);
                productVersion = args.get("product-version", 100);
                mdrConfig = new MdrConfig();
                mdrConfig.setIndexOptions(args);

                try {
                        imgFs = createGmapsupp();
                } catch (FileNotWritableException e) {
                        throw new MapFailedException("Could not create gmapsupp.img file");
                }
        }

        /**
         * 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.
         * @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) {
                MdrBuilder mdrBuilder = mdrBuilderMap.get(familyId);
                if (mdrBuilder != null)
                        return mdrBuilder;

                mdrBuilder = new MdrBuilder();

                try {
                        String imgname = String.format("%08d.MDR", familyId);
                        ImgChannel chan = imgFs.create(imgname);
                        mdrBuilder.initForDevice(chan, sort, mdrConfig);
                } catch (FileExistsException e) {
                        Logger.defaultLogger.error("Could not create duplicate MDR file");
                }

                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())
                                Logger.defaultLogger.warn("Input file '" + info.getFilename() + "' has a different code page (" + sort.getCodepage() + " rather than " + prevSort.getCodepage() + ")");
                        if (info.hasSortOrder() && prevSort.getSortOrderId() != sort.getSortOrderId())
                                Logger.defaultLogger.warn("Input file '" + info.getFilename() + "' has a different sort order (" + sort.getSortOrderId() + " rather than " + 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());
                                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();
                }

                try {

                        addAllFiles(imgFs);

                        writeSrtFile(imgFs);
                        writeMpsFile();

                } catch (FileNotWritableException e) {
                        Logger.defaultLogger.error("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;

                        try {
                                ImgChannel channel = imgFs.create(String.format("%08d.SRT", familyId));
                                SRTFile srtFile = new SRTFile(channel);
                                srtFile.setSort(sort);
                                srtFile.write();
                                // Do not close srtFile here
                        } catch (FileExistsException e) {
                                // well it shouldn't exist!
                                Logger.defaultLogger.error("could not create SRT file as it exists already");
                                throw new FileNotWritableException("already existed", e);
                        }
                }
        }

        /**
         * 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();
                } 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 static 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()) {

                        switch (info.getKind()) {
                        case IMG_KIND:
                                addImg(outfs, info);
                                addMpsEntry(info);
                                break;
                        case GMAPSUPP_KIND:
                                addImg(outfs, info);
                                addMpsFile(info);
                                break;
                        case APP_KIND:
                        case TYP_KIND:
                                addFile(outfs, info);
                                break;
                        default:
                                break;
                        }
                }
        }

        private static void addImg(FileSystem outfs, FileInfo info) {
                FileCopier fc = new FileCopier(info.getFilename());
                List<SubFileInfo> subFiles = info.subFiles();

                for (SubFileInfo sf : subFiles) {
                        try {
                                ImgChannel chan = outfs.create(sf.getName());
                                Closeable sync = fc.add(sf.getName(), chan);

                                ((FileLink)chan).link(sf, sync);
                        } catch (FileExistsException e) {
                                Logger.defaultLogger.warn("Could not copy " + sf.getName(), e);
                        }
                }
        }

        private static void addFile(FileSystem outfs, FileInfo info) {
                String filename = info.getFilename();
                FileCopier fc = new FileCopier(filename);

                try {
                        ImgChannel chan = outfs.create(createImgFilename(filename));
                        ((FileLink) chan).link(info.subFiles().get(0), fc.file(chan));
                } catch (FileExistsException e) {
                        Logger.defaultLogger.warn("Could not copy " + filename, e);
                }
        }

        /**
         * 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();
                try (FileSystem 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) {
                        Logger.defaultLogger.error("Could not read MPS file from gmapsupp", e);
                }
        }

        /**
         * 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 static 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!
                        Logger.defaultLogger.error("could not create MPS file as it already exists");
                        throw new FileNotWritableException("already existed", e);
                }
        }


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

        /**
         * Create the output file.
         *
         * @return The gmapsupp file.
         * @throws FileNotWritableException If it cannot be created for any reason.
         */

        private FileSystem createGmapsupp() throws FileNotWritableException {

                // Create this file, containing all the sub files
                FileSystemParam params = new FileSystemParam();
                params.setMapDescription(overallDescription);
                params.setGmapsupp(true);
                params.setHideGmapsuppOnPC(hideGmapsuppOnPC);
                params.setProductVersion(productVersion);

                FileSystem outfs = ImgFS.createFs(Utils.joinPath(outputDir, GMAPSUPP), params);

                mpsFile = createMpsFile(outfs);
                mpsFile.setMapsetName(mapsetName);

                return outfs;
        }

        public void setCreateIndex(boolean create) {
                this.createIndex = create;
        }
}

/**
 * Copies files from the source img to the gmapsupp.
 *
 * Each sub file has to be copied separately to a different 'file'.  This class makes sure
 * that the source file is only opened once.
 */

class FileCopier {
        private final String filename;
        private FileSystem fs;
        private int refCount;

        public FileCopier(String filename) {
                this.filename = filename;
        }

        Closeable add(String name, ImgChannel fout) {
                refCount++;
                return () -> sync(name, fout);
        }

        Closeable file(ImgChannel fout) {
                return () -> sync(fout);
        }

        /**
         * This version of sync() is used for single files.
         */

        public void sync(ImgChannel fout) throws IOException {
                try (ImgChannel fin = new FileImgChannel(filename, "r")) {
                        copyFile(fin, fout);
                }
        }

        /**
         * This version of sync is used for subfiles within a .img file.
         * @param name The sub file name.
         * @param fout Where to copy the file
         */

        void sync(String name, ImgChannel fout) throws IOException {
                if (fs == null)
                        fs = ImgFS.openFs(filename);

                copyFile(name, fout);

                refCount--;
                if (refCount <= 0) {
                        fs.close();
                }
        }

        private void copyFile(String name, ImgChannel fout) throws IOException {
                try (ImgChannel fin = fs.open(name, "r")) {
                        copyFile(fin, fout);
                }
        }

        private static void copyFile(ImgChannel fin, ImgChannel fout) throws IOException {
                ByteBuffer buf = ByteBuffer.allocate(1024);
                while (fin.read(buf) > 0) {
                        buf.flip();
                        fout.write(buf);
                        buf.compact();
                }
        }
}