Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2007 - 2012.
 *
 * 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.main;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.app.srt.Sort;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.ArgumentProcessor;
import uk.me.parabola.mkgmap.CommandArgs;
import uk.me.parabola.mkgmap.CommandArgsReader;
import uk.me.parabola.mkgmap.Version;
import uk.me.parabola.mkgmap.combiners.Combiner;
import uk.me.parabola.mkgmap.combiners.FileInfo;
import uk.me.parabola.mkgmap.combiners.GmapsuppBuilder;
import uk.me.parabola.mkgmap.combiners.MdrBuilder;
import uk.me.parabola.mkgmap.combiners.MdxBuilder;
import uk.me.parabola.mkgmap.combiners.NsisBuilder;
import uk.me.parabola.mkgmap.combiners.OverviewBuilder;
import uk.me.parabola.mkgmap.combiners.OverviewMap;
import uk.me.parabola.mkgmap.combiners.TdbBuilder;
import uk.me.parabola.mkgmap.osmstyle.StyleFileLoader;
import uk.me.parabola.mkgmap.osmstyle.StyleImpl;
import uk.me.parabola.mkgmap.reader.osm.Style;
import uk.me.parabola.mkgmap.reader.osm.StyleInfo;
import uk.me.parabola.mkgmap.reader.overview.OverviewMapDataSource;
import uk.me.parabola.mkgmap.scan.SyntaxException;
import uk.me.parabola.mkgmap.srt.SrtTextReader;
import uk.me.parabola.util.EnhancedProperties;

/**
 * The new main program.  There can be many file names to process and there can
 * be differing outputs determined by options.  So the actual work is mostly
 * done in other classes.  This one just works out what is wanted.
 *
 * @author Steve Ratcliffe
 */

public class Main implements ArgumentProcessor {
        private static final Logger log = Logger.getLogger(Main.class);

        // Final .img file combiners.
        private final List<Combiner> combiners = new ArrayList<Combiner>();

        private final Map<String, MapProcessor> processMap = new HashMap<String, MapProcessor>();
        private String styleFile = "classpath:styles";
        private String styleOption;
        private boolean verbose;

        private final List<FilenameTask> futures = new LinkedList<FilenameTask>();
        private ExecutorService threadPool;
        // default number of threads
        private int maxJobs = 1;

        private boolean createTdbFiles = false;
        private boolean tdbBuilderAdded = false;
        // used for messages in listStyles and checkStyles
        private String searchedStyleName;

        private volatile int programRC = 0;

        /**
         * Used for unit tests
         * @param args
         */

        public static void mainNoSystemExit(String[] args) {
                Main.mainStart(args);
        }
       
        public static void main(String[] args) {
                int rc = Main.mainStart(args);
                if (rc != 0)
                        System.exit(1);
        }
       
        /**
         * The main program to make or combine maps.  We now use a two pass process,
         * first going through the arguments and make any maps and collect names
         * to be used for creating summary files like the TDB and gmapsupp.
         *
         * @param args The command line arguments.
         */

        private static int mainStart(String[] args) {
                long start = System.currentTimeMillis();
                System.out.println("Time started: " + new Date());
                // We need at least one argument.
                if (args.length < 1) {
                        printUsage();
                        printHelp(System.err, getLang(), "options");
                        return 0;
                }

                Main mm = new Main();

                int numExitExceptions = 0;
                try {
                        // Read the command line arguments and process each filename found.
                        CommandArgsReader commandArgs = new CommandArgsReader(mm);
                        commandArgs.setValidOptions(getValidOptions(System.err));
                        commandArgs.readArgs(args);
                } catch (MapFailedException e) {
                        System.err.println(e.getMessage()); // should not happen
                } catch (ExitException e) {
                        ++numExitExceptions;
                        System.err.println(e.getMessage());
                }
               
                System.out.println("Number of ExitExceptions: " + numExitExceptions);
               
                System.out.println("Time finished: " + new Date());
                System.out.println("Total time taken: " + (System.currentTimeMillis() - start) + "ms");
                if (numExitExceptions > 0 || mm.getProgramRC() != 0){
                        return 1;
                }
                return 0;
        }
       
        private static void printUsage (){
                System.err.println("Usage: mkgmap [options...] <file.osm>");
        }

        private void setProgramRC(int rc){
                programRC = rc;
        }

        private int getProgramRC(){
                return programRC;
        }
       
        /**
         * Grab the options help file and print it.
         * @param err The output print stream to write to.
         * @param lang A language hint.  The help will be displayed in this
     * language if it has been translated.
         * @param file The help file to display.
         */

        private static void printHelp(PrintStream err, String lang, String file) {
                String path = "/help/" + lang + '/' + file;
                InputStream stream = Main.class.getResourceAsStream(path);
                if (stream == null) {
                        err.println("Could not find the help topic: " + file + ", sorry");
                        return;
                }

                BufferedReader r = new BufferedReader(new InputStreamReader(stream));
                try {
                        String line;
                        while ((line = r.readLine()) != null)
                                err.println(line);
                } catch (IOException e) {
                        err.println("Could not read the help topic: " + file + ", sorry");
                }
        }

        private static Set<String> getValidOptions(PrintStream err) {
                String path = "/help/en/options";
                InputStream stream = Main.class.getResourceAsStream(path);
                if (stream == null)
                        return null;

                Set<String> result = new HashSet<String>();
                try {
                        BufferedReader r = new BufferedReader(new InputStreamReader(stream, "utf-8"));

                        Pattern p = Pattern.compile("^--?([a-zA-Z0-9-]*).*$");
                        String line;
                        while ((line = r.readLine()) != null) {
                                Matcher matcher = p.matcher(line);
                                if (matcher.matches()) {
                                        String opt = matcher.group(1);
                                        result.add(opt);
                                }
                        }
                } catch (IOException e) {
                        err.println("Could not read valid optoins");
                        return null;
                }

                return result;
        }

        public void startOptions() {
                MapProcessor saver = new NameSaver();
                processMap.put("img", saver);
                processMap.put("mdx", saver);

                processMap.put("typ", new TypSaver());

                // Normal map files.
                processMap.put("rgn", saver);
                processMap.put("tre", saver);
                processMap.put("lbl", saver);
                processMap.put("net", saver);
                processMap.put("nod", saver);

                processMap.put("txt", new TypCompiler());
        }

        /**
         * Switch out to the appropriate class to process the filename.
         */

        public void processFilename(final CommandArgs args, final String filename) {
               
                final String ext = extractExtension(filename);
                log.debug("file", filename, ", extension is", ext);
                // ignore ovm_* files given as command line arguments
                if (OverviewBuilder.isOverviewImg(filename))
                        return;
               
                final MapProcessor mp = mapMaker(ext);

                args.setSort(getSort(args));

                log.info("Submitting job " + filename);
                FilenameTask task = new FilenameTask(new Callable<String>() {
                        public String call() {
                                log.threadTag(filename);
                                if (filename.startsWith("test-map:") || new File(filename).exists()){
                                        String output = mp.makeMap(args, filename);
                                        log.debug("adding output name", output);
                                        log.threadTag(null);
                                        return output;
                                } else {
                                        log.error("file " + filename + " doesn't exist");
                                        return null;
                                }
                        }
                });
                task.setArgs(args);
                futures.add(task);
        }

        private MapProcessor mapMaker(String ext) {
                MapProcessor mp = processMap.get(ext);
                if (mp == null)
                        mp = new MapMaker(createTdbFiles);
                return mp;
        }

        public void processOption(String opt, String val) {
                log.debug("option:", opt, val);

                if (opt.equals("number-of-files")) {

                        // This option always appears first.  We use it to turn on/off
                        // generation of the overview files if there is only one file
                        // to process.
                        int n = Integer.valueOf(val);
                        if (n > 0) // TODO temporary, this option will become properly default of on.
                                createTdbFiles = true;

                } else if (opt.equals("help")) {
                        printHelp(System.out, getLang(), (!val.isEmpty()) ? val : "help");
                } else if (opt.equals("style-file") || opt.equals("map-features")) {
                        styleFile = val;
                } else if (opt.equals("style")) {
                        styleOption = val;
                } else if (opt.equals("verbose")) {
                        verbose = true;
                } else if (opt.equals("list-styles")) {
                        listStyles();
                } else if (opt.equals("check-styles")) {
                        checkStyles();
                } else if (opt.equals("max-jobs")) {
                        if (val.isEmpty())
                                maxJobs = Runtime.getRuntime().availableProcessors();
                        else
                                maxJobs = Integer.parseInt(val);
                        if(maxJobs < 1) {
                                log.warn("max-jobs has to be at least 1");
                                maxJobs = 1;
                        }
                } else if (opt.equals("version")) {
                        System.err.println(Version.VERSION);
                        System.exit(0);
                }
        }

        public void removeOption(String opt) {
                if ("tdbfile".equals(opt))
                        createTdbFiles = false;
        }

        /**
         * Add the builders for the TDB and overview map.  These are always
         * generated together as we use some info that is calculated when constructing
         * the overview map in the TDB file.
         */

        private void addTdbBuilder() {
                if (!tdbBuilderAdded ){
                        OverviewMap overviewSource = new OverviewMapDataSource();
                        OverviewBuilder overviewBuilder = new OverviewBuilder(overviewSource);
                        addCombiner(overviewBuilder);
                        TdbBuilder tdbBuilder = new TdbBuilder(overviewBuilder);
                        addCombiner(tdbBuilder);
                        tdbBuilderAdded = true;
                }
        }

        private void listStyles() {
                String[] names;
                try {
                        StyleFileLoader loader = StyleFileLoader.createStyleLoader(styleFile, null);
                        names = loader.list();
                        loader.close();
                } catch (FileNotFoundException e) {
                        log.debug("didn't find style file", e);
                        throw new ExitException("Could not list style file " + styleFile);
                }

                Arrays.sort(names);
                System.out.println("The following styles are available:");
                for (String name : names) {
                        Style style = readOneStyle(name, false);
                        if (style == null)
                                continue;
                        StyleInfo info = style.getInfo();
                        System.out.format("%-15s %6s: %s\n",
                                        searchedStyleName,info.getVersion(), info.getSummary());
                        if (verbose) {
                                for (String s : info.getLongDescription().split("\n"))
                                        System.out.printf("\t%s\n", s.trim());
                        }
                }
        }
 
        /**
         * Check one or all styles in the path given in styleFile.
         */

        private void checkStyles() {
                String[] names;
                int checked = 0;
                try {
                        StyleFileLoader loader = StyleFileLoader.createStyleLoader(styleFile, null);
                        names = loader.list();
                        loader.close();
                } catch (FileNotFoundException e) {
                        log.debug("didn't find style file", e);
                        throw new ExitException("Could not check style file " + styleFile);
                }

                Arrays.sort(names);
               
                if (styleOption == null){
                        if (names.length > 1)
                                System.out.println("The following styles are available:");
                        else
                                System.out.println("Found one style in " + styleFile);
                }
                for (String name : names) {
                        if (styleOption != null && name.equals(styleOption) == false)
                                continue;
                        if (names.length > 1){
                                System.out.println("checking style: " + name);
                        }
                        ++checked;
                        boolean performChecks = true;
                        if ("classpath:styles".equals(styleFile) && "default".equals(name) == false){
                                        performChecks = false;
                        }
                        Style style = readOneStyle(name, performChecks);
                        if (style == null){
                                System.out.println("could not open style " + name);
                        }
                }
                if (checked == 0)
                        System.out.println("could not open style " + styleOption + " in " + styleFile );
                System.out.println("finished check-styles");
        }

        /**
         * Try to read a style from styleFile directory
         * @param name name of the style
         * @param performChecks perform checks?
         * @return the style or null in case of errors
         */

        private Style readOneStyle(String name, boolean performChecks){
                Style style = null;
                searchedStyleName = name;
                try {
                        style = new StyleImpl(styleFile, name, new EnhancedProperties(), performChecks);
                } catch (SyntaxException e) {
                        System.err.println("Error in style: " + e.getMessage());
                } catch (FileNotFoundException e) {
                        log.debug("could not find style", name);
                        try {
                                searchedStyleName = new File(styleFile).getName();
                                style = new StyleImpl(styleFile, null, new EnhancedProperties(), performChecks);
                        } catch (SyntaxException e1) {
                                System.err.println("Error in style: " + e1.getMessage());
                        } catch (FileNotFoundException e1) {
                                log.debug("could not find style", styleFile);
                        }
                }
                return style;
        }
       
        private static String getLang() {
                return "en";
        }

        private void addCombiner(Combiner combiner) {
                combiners.add(combiner);
        }

        public void endOptions(CommandArgs args) {
                fileOptions(args);

                log.info("Start tile processors");
                if (threadPool == null) {
                        log.info("Creating thread pool with " + maxJobs + " threads");
                        threadPool = Executors.newFixedThreadPool(maxJobs);
                }

                // process all input files
                for (FilenameTask task : futures) {
                        threadPool.execute(task);
                }


                List<FilenameTask> filenames = new ArrayList<FilenameTask>();
               
                int numMapFailedExceptions = 0;
               
                if (threadPool != null) {
                        threadPool.shutdown();
                        while (!futures.isEmpty()) {
                                try {
                                        try {
                                                // don't call get() until a job has finished
                                                if (futures.get(0).isDone()) {
                                                        FilenameTask future = futures.remove(0);

                                                        // Provoke any exceptions by calling get and then
                                                        // save the result for later use
                                                        future.setFilename(future.get());
                                                        filenames.add(future);
                                                } else
                                                        Thread.sleep(100);
                                        } catch (ExecutionException e) {
                                                // Re throw the underlying exception
                                                Throwable cause = e.getCause();
                                                if (cause instanceof Exception)
                                                        //noinspection ProhibitedExceptionThrown
                                                        throw (Exception) cause;
                                                else if (cause instanceof Error)
                                                        //noinspection ProhibitedExceptionThrown
                                                        throw (Error) cause;
                                                else
                                                        throw e;
                                        }
                                } catch (ExitException ee) {
                                        throw ee;
                                } catch (MapFailedException mfe) {
//                                      System.err.println(mfe.getMessage()); // already printed via log
                                        numMapFailedExceptions++;
                                        setProgramRC(-1);
                                } catch (Throwable t) {
                                        t.printStackTrace();
                                        if (!args.getProperties().getProperty("keep-going", false)) {
                                                throw new ExitException("Exiting - if you want to carry on regardless, use the --keep-going option");
                                        }
                                }
                        }
                }
                System.out.println("Number of MapFailedExceptions: " + numMapFailedExceptions);

                if (combiners.isEmpty())
                        return;
                boolean hasFiles = false;
                for (FilenameTask file : filenames) {
                        if (file == null || file.isCancelled() || file.getFilename() == null){
                                if (args.getProperties().getProperty("keep-going", false))
                                        continue;
                                else
                                        throw new ExitException("Exiting - if you want to carry on regardless, use the --keep-going option");
                        }
                        hasFiles = true;
                }
                if (!hasFiles){
                        log.warn("nothing to do for combiners.");
                        return;
                }
                log.info("Combining maps");

                args.setSort(getSort(args));

                // Get them all set up.
                for (Combiner c : combiners)
                        c.init(args);

                // will contain img files for which an additional ovm file was found  
                HashSet<String> foundOvmFiles = new HashSet<String>();
                // try OverviewBuilder with special files  
                if (tdbBuilderAdded){
                        for (FilenameTask file : filenames) {
                                if (file == null || file.isCancelled())
                                        continue;

                                try {
                                        String fileName = file.getFilename();
                                        if (fileName.endsWith(".img") == false)
                                                continue;
                                        fileName = OverviewBuilder.getOverviewImgName(fileName);
                                        log.info("  " + fileName);
                                        FileInfo fileInfo = FileInfo.getFileInfo(fileName);
                                        fileInfo.setArgs(file.getArgs());
                                        // add the real input file
                                        foundOvmFiles.add(file.getFilename());
                                        for (Combiner c : combiners){
                                                if (c instanceof OverviewBuilder)
                                                        c.onMapEnd(fileInfo);
                                        }
                                } catch (FileNotFoundException e) {
                                }
                        }
                }
               
                // Tell them about each filename (OverviewBuilder excluded)
                for (FilenameTask file : filenames) {
                        if (file == null || file.isCancelled())
                                continue;

                        try {
                                log.info("  " + file);
                                FileInfo fileInfo = FileInfo.getFileInfo(file.getFilename());
                                fileInfo.setArgs(file.getArgs());
                                for (Combiner c : combiners){
                                        if (c instanceof OverviewBuilder && foundOvmFiles.contains(file.getFilename()))
                                                continue;
                                        c.onMapEnd(fileInfo);
                                }
                        } catch (FileNotFoundException e) {
                                throw new MapFailedException("could not open file " + e.getMessage());
                        }
                }
               

                // All done, allow tidy up or file creation to happen
                for (Combiner c : combiners)
                        c.onFinish();
               
                if (tdbBuilderAdded && args.getProperties().getProperty("remove-ovm-work-files", false)){
                        for (String fName:foundOvmFiles){
                                String ovmFile = OverviewBuilder.getOverviewImgName(fName);
                                log.info("removing " + ovmFile);
                                new File(ovmFile).delete();
                        }
                }
        }

        private void fileOptions(CommandArgs args) {
                boolean indexOpt = args.exists("index");
                boolean gmapOpt = args.exists("gmapsupp");
                boolean tdbOpt = args.exists("tdbfile");
                if (tdbOpt || createTdbFiles){
                        addTdbBuilder();
                }
                if (args.exists("nsis")) {
                        addCombiner(new NsisBuilder());
                }
                if (gmapOpt) {
                        GmapsuppBuilder gmapBuilder = new GmapsuppBuilder();
                        gmapBuilder.setCreateIndex(indexOpt);

                        addCombiner(gmapBuilder);
                }

                if (indexOpt && (tdbOpt || !gmapOpt)) {
                        addCombiner(new MdrBuilder());
                        addCombiner(new MdxBuilder());
                }
        }

        /**
         * Get the extension of the filename, ignoring any compression suffix.
         *
         * @param filename The original filename.
         * @return The file extension.
         */

        private static String extractExtension(String filename) {
                String[] parts = filename.toLowerCase(Locale.ENGLISH).split("\\.");
                List<String> ignore = Arrays.asList("gz", "bz2", "bz");

                // We want the last part that is not gz, bz etc (and isn't the first part ;)
                for (int i = parts.length - 1; i > 0; i--) {
                        String ext = parts[i];
                        if (!ignore.contains(ext))
                                return ext;
                }
                return "";
        }

        /**
         * Create the sort description for the map.  This is used to sort items in the files
         * and also is converted into a SRT file which is included in the MDR file.
         *
         * We simply use the code page to locate a sorting description, but it would be possible to
         * specify the sort separately.
         *
         * @return A sort description object.
         */

        public Sort getSort(CommandArgs args) {
                return SrtTextReader.sortForCodepage(args.getCodePage());
        }

        /**
         * A null implementation that just returns the input name as the output.
         */

        private static class NameSaver implements MapProcessor {
                public String makeMap(CommandArgs args, String filename) {
                        return filename;
                }
        }

        private static class FilenameTask extends FutureTask<String> {
                private CommandArgs args;
                private String filename;

                private FilenameTask(Callable<String> callable) {
                        super(callable);
                }

                public void setArgs(CommandArgs args) {
                        this.args = args;
                }

                public CommandArgs getArgs() {
                        return args;
                }

                public void setFilename(String filename) {
                        this.filename = filename;
                }

                public String getFilename() {
                        return filename;
                }

                public String toString() {
                        return filename;
                }
        }
}