Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2010.
 *
 * 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.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Formatter;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import uk.me.parabola.imgfmt.FormatException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.net.GeneralRouteRestriction;
import uk.me.parabola.imgfmt.app.net.RoadDef;
import uk.me.parabola.mkgmap.general.LevelInfo;
import uk.me.parabola.mkgmap.general.MapCollector;
import uk.me.parabola.mkgmap.general.MapElement;
import uk.me.parabola.mkgmap.general.MapLine;
import uk.me.parabola.mkgmap.general.MapPoint;
import uk.me.parabola.mkgmap.general.MapRoad;
import uk.me.parabola.mkgmap.general.MapShape;
import uk.me.parabola.mkgmap.osmstyle.ActionRule;
import uk.me.parabola.mkgmap.osmstyle.ExpressionRule;
import uk.me.parabola.mkgmap.osmstyle.StyleFileLoader;
import uk.me.parabola.mkgmap.osmstyle.StyleImpl;
import uk.me.parabola.mkgmap.osmstyle.StyledConverter;
import uk.me.parabola.mkgmap.osmstyle.TypeReader;
import uk.me.parabola.mkgmap.osmstyle.actions.ActionList;
import uk.me.parabola.mkgmap.osmstyle.actions.ActionReader;
import uk.me.parabola.mkgmap.osmstyle.eval.ExpressionReader;
import uk.me.parabola.mkgmap.osmstyle.eval.Op;
import uk.me.parabola.mkgmap.reader.osm.Element;
import uk.me.parabola.mkgmap.reader.osm.ElementSaver;
import uk.me.parabola.mkgmap.reader.osm.FeatureKind;
import uk.me.parabola.mkgmap.reader.osm.GType;
import uk.me.parabola.mkgmap.reader.osm.Node;
import uk.me.parabola.mkgmap.reader.osm.OsmConverter;
import uk.me.parabola.mkgmap.reader.osm.Relation;
import uk.me.parabola.mkgmap.reader.osm.Rule;
import uk.me.parabola.mkgmap.reader.osm.Style;
import uk.me.parabola.mkgmap.reader.osm.TypeResult;
import uk.me.parabola.mkgmap.reader.osm.WatchableTypeResult;
import uk.me.parabola.mkgmap.reader.osm.Way;
import uk.me.parabola.mkgmap.reader.osm.xml.Osm5XmlHandler;
import uk.me.parabola.mkgmap.reader.osm.xml.Osm5XmlHandler.SaxHandler;
import uk.me.parabola.mkgmap.scan.SyntaxException;
import uk.me.parabola.mkgmap.scan.Token;
import uk.me.parabola.mkgmap.scan.TokenScanner;
import uk.me.parabola.util.EnhancedProperties;

import org.xml.sax.SAXException;


/**
 * Test style rules by converting to a text format, rather than a .img file.
 * In addition you can specify a .osm file and a style file separately.
 *
 * <h2>Single test file</h2>
 * The format of the file is as follows
 *
 * <pre>
 * WAY 42
 * highway=primary
 * oneway=reverse
 *
 * <<<lines>>>
 * highway=primary [0x3 road_class=2 road_speed=2]
 * power=line [0x29 resolution 20]
 * </pre>
 *
 * You can have any number of ways, each must end with a blank line.
 * A way will be created with two points (1,1),(2,2) (so you can see the
 * action of oneway=reverse) and the tags that you specify.  If you give
 * a number after WAY it will be printed on output so that if you have more
 * than one you can tell which is which.  If the number is omitted it will
 * default to 1.
 *
 * You can have as many rules as you like after the <<<lines>>> and you
 * can include any other style files such as <<<options>>> or <<<info>>> if
 * you like.
 *
 * <h2>osm file mode</h2>
 * Takes two arguments, first the style file and then the osm file.
 *
 * You can give a --reference flag and it will run style file in reference mode,
 * that is each rule will be applied to the element without any attempt at
 * optimisation.  This acts as an independent check of the main style code
 * which may have more optimisations.
 *
 * @author Steve Ratcliffe
 */

public class StyleTester implements OsmConverter {
        private static final Pattern SPACES_PATTERN = Pattern.compile(" +");
        private static final Pattern EQUAL_PATTERN = Pattern.compile("=");

        private static final String STYLETESTER_STYLE = "styletester.style";

        private static PrintStream out = System.out;
        private static boolean reference;

        private final OsmConverter converter;

        // The file may contain a known good set of results.  They are saved here
        private final List<String> givenResults = new ArrayList<String>();
        private static boolean forceUseOfGiven;
        private static boolean showMatches;
        private static boolean print = true;

        private StyleTester(String stylefile, MapCollector coll, boolean reference) throws FileNotFoundException {
                if (reference)
                        converter = makeStrictStyleConverter(stylefile, coll);
                else
                        converter = makeStyleConverter(stylefile, coll);
        }

        public static void main(String[] args) throws IOException {
                String[] a = processOptions(args);
                if (a.length == 1)
                        runSimpleTest(a[0]);
                else
                        runTest(a[0], a[1]);
        }

        public static void setOut(PrintStream out) {
                StyleTester.out = out;
        }

        private static String[] processOptions(String[] args) {
                List<String> a = new ArrayList<String>();
                for (String s : args) {
                        if (s.startsWith("--reference")) {
                                System.out.println("# using reference method of calculation");
                                reference = true;
                        } else if (s.startsWith("--show-matches")) {
                                if (!reference)
                                        System.out.println("# using reference method of calculation");
                                reference = true;
                                showMatches = true;
                        } else if (s.startsWith("--no-print")) {
                                print = false;
                        } else
                                a.add(s);
                }
                return a.toArray(new String[a.size()]);
        }

        private static void runTest(String stylefile, String mapfile) {
                PrintingMapCollector collector = new PrintingMapCollector();
                OsmConverter normal;
                try {
                        normal = new StyleTester(stylefile, collector, reference);
                } catch (FileNotFoundException e) {
                        System.err.println("Could not open style file " + stylefile);
                        return;
                }
                try {

                        InputStream is = Utils.openFile(mapfile);
                        SAXParserFactory parserFactory = SAXParserFactory.newInstance();
                        parserFactory.setXIncludeAware(true);
                        parserFactory.setNamespaceAware(true);
                        SAXParser parser = parserFactory.newSAXParser();

                        try {
                                EnhancedProperties props = new EnhancedProperties();
                                props.put("preserve-element-order", "1");
                                ElementSaver saver = new ElementSaver(props);
                                Osm5XmlHandler handler = new Osm5XmlHandler(props);
                                SaxHandler saxHandler = handler.new SaxHandler();
                                handler.setElementSaver(saver);
                                parser.parse(is, saxHandler);
                                saver.finishLoading();
                                saver.convert(normal);

                                System.err.println("Conversion time " + (System.currentTimeMillis() - collector.getStart()) + "ms");
                        } catch (IOException e) {
                                throw new FormatException("Error reading file", e);
                        }
                } catch (SAXException e) {
                        throw new FormatException("Error parsing file", e);
                } catch (ParserConfigurationException e) {
                        throw new FormatException("Internal error configuring xml parser", e);
                } catch (FileNotFoundException e) {
                        System.err.println("Cannot open file " + mapfile);
                }
        }
       

        /**
         * Run a simple test with a combined test file.
         * @param filename The test file contains text way definitions and a style
         * file all in one.
         */

        public static void runSimpleTest(String filename) {
                try {
                        FileReader reader = new FileReader(filename);
                        BufferedReader br = new BufferedReader(reader);
                        List<Way> ways = readSimpleTestFile(br);

                        List<MapElement> results = new ArrayList<MapElement>();

                        List<MapElement> strictResults = new ArrayList<MapElement>();

                        OsmConverter strict = new StyleTester("styletester.style", new LocalMapCollector(strictResults), true);
                        List<String> givenList = ((StyleTester) strict).givenResults;

                        List<String> all = new ArrayList<String>();
                        for (Way w : ways) {
                                OsmConverter normal = new StyleTester("styletester.style", new LocalMapCollector(results), false);
                                strict = new StyleTester("styletester.style", new LocalMapCollector(strictResults), true);

                                String prefix = "WAY " + w.getId() + ": ";
                                normal.convertWay(w.copy());
                                normal.end();
                                String[] actual = formatResults(prefix, results);
                                all.addAll(Arrays.asList(actual));
                                results.clear();

                                strict.convertWay(w.copy());
                                strict.end();
                                String[] expected = formatResults(prefix, strictResults);
                                strictResults.clear();

                                printResult(actual);

                                if (!Arrays.deepEquals(actual, expected)) {
                                        out.println("ERROR expected result is:");
                                        printResult(expected);
                                }

                                out.println();
                        }

                        String[] given = givenList.toArray(new String[givenList.size()]);
                        if ((given.length > 0 || forceUseOfGiven) && !Arrays.deepEquals(all.toArray(), givenList.toArray())) {
                                out.println("ERROR given results were:");
                                printResult(given);
                        }
                } catch (FileNotFoundException e) {
                        System.err.println("Cannot open test file " + filename);
                } catch (IOException e) {
                        System.err.println("Failure while reading test file " + filename);
                }
        }


        public void convertWay(Way way) {
                converter.convertWay(way);
        }

        public void convertNode(Node node) {
                converter.convertNode(node);
        }

        public void convertRelation(Relation relation) {
                converter.convertRelation(relation);
        }

        public void setBoundingBox(Area bbox) {
                converter.setBoundingBox(bbox);
        }

        public void end() {
                converter.end();
        }

        @Override
        public Boolean getDriveOnLeft() {
                return null; // unknown
        }

        private static void printResult(String[] results) {
                for (String s : results) {
                        out.println(s);
                }
        }

        /**
         * Read in the combined test file.  This contains some ways and a style.
         * The style does not need to include 'version' as this is added for you.
         */

        private static List<Way> readSimpleTestFile(BufferedReader br) throws IOException {
                List<Way> ways = new ArrayList<Way>();

                String line;
                while ((line = br.readLine()) != null) {
                        line = line.trim();
                        if (line.toLowerCase(Locale.ENGLISH).startsWith("way")) {
                                Way w = readWayTags(br, line);
                                ways.add(w);
                        } else if (line.startsWith("<<<")) {
                                // read the rest of the file
                                readStyles(br, line);
                        }
                        /*else if ("".equals(line) || line.startsWith("#")) {
                                // ignore blank lines.
                        }*/

                }
                br.close();

                return ways;
        }

        /**
         * You can have a number of ways defined in the file.  If you give a
         * number after 'way' that is used as the way id so that you can identify
         * it in the results.
         *
         * A list of tags are read and added to the way up until a blank line.
         *
         * @param br Read from here.
         * @param waydef This will contain the way-id if one was given.  Otherwise
         * the way id will be 1.
         * @throws IOException If the file cannot be read.
         */

        private static Way readWayTags(BufferedReader br, String waydef) throws IOException {
                int id = 1;
                String[] strings = SPACES_PATTERN.split(waydef);
                if (strings.length > 1)
                        id = Integer.parseInt(strings[1]);

                Way w = new Way(id);
                w.addPoint(new Coord(1, 1));
                w.addPoint(new Coord(2, 2));

                String line;
                while ((line = br.readLine()) != null) {
                        if (line.indexOf('=') < 0)
                                break;
                        String[] tagval = EQUAL_PATTERN.split(line, 2);
                        if (tagval.length == 2)
                                w.addTag(tagval[0], tagval[1]);
                }

                return w;
        }

        /**
         * Print out the garmin elements that were produced by the rules.
         * @param prefix This string will be prepended to the formatted result.
         * @param lines The resulting map elements.
         */

        private static String[] formatResults(String prefix, List<MapElement> lines) {
                String[] result = new String[lines.size()];
                int i = 0;
                for (MapElement el : lines) {
                        String s;
                        // So we can run against versions that do not have toString() methods
                        if (el instanceof MapRoad)
                                s = roadToString((MapRoad) el);
                        else
                                s = lineToString((MapLine) el);
                        result[i++] = prefix + s;
                }
                return result;
        }

        /**
         * This is so we can run against versions of mkgmap that do not have
         * toString methods on MapLine and MapRoad.
         */

        private static String lineToString(MapLine el) {
                Formatter fmt = new Formatter();
                fmt.format("Line 0x%x, labels=%s, res=%d-%d",
                                el.getType(), Arrays.toString(el.getLabels()),
                                el.getMinResolution(), el.getMaxResolution());
                if (el.isDirection())
                        fmt.format(" oneway");

                fmt.format(" ");
                for (Coord co : el.getPoints())
                        fmt.format("(%s),", co);

                return fmt.toString();
        }

        /**
         * This is so we can run against versions of mkgmap that do not have
         * toString methods on MapLine and MapRoad.
         */

        private static String roadToString(MapRoad el) {
                StringBuffer sb = new StringBuffer(lineToString(el));
                sb.delete(0, 4);
                sb.insert(0, "Road");
                Formatter fmt = new Formatter(sb);
                fmt.format(" road class=%d speed=%d", el.getRoadDef().getRoadClass(),
                                getRoadSpeed(el.getRoadDef()));
                return fmt.toString();
        }

        /**
         * Implement a method to get the road speed from RoadDef.
         */

        private static int getRoadSpeed(RoadDef roadDef) {
                try {
                        Field field = RoadDef.class.getDeclaredField("tabAInfo");
                        field.setAccessible(true);
                        int tabA = (Integer) field.get(roadDef);
                        return tabA & 0x7;
                } catch (NoSuchFieldException e) {
                        e.printStackTrace();
                } catch (IllegalAccessException e) {
                        e.printStackTrace();
                }
                return 0;
        }

        /**
         * Read the style definitions.  The rest of the file is just copied to
         * a style file named 'styletester.style' so that it can be read in the
         * normal manner.
         * @param br Read from here.
         * @param initLine The first line of the style definition that has already been read.
         * @throws IOException If writing fails.
         */

        private static void readStyles(BufferedReader br, String initLine) throws IOException {
                FileWriter writer = new FileWriter(STYLETESTER_STYLE);
                PrintWriter pw = new PrintWriter(writer);

                pw.println("<<<version>>>\n0");
                pw.println(initLine);

                try {
                        String line;
                        while ((line = br.readLine()) != null)
                                pw.println(line);
                } finally {
                        pw.close();
                }

        }

        /**
         * A styled converter that should work exactly the same as the version of
         * mkgmap you are using.
         * @param styleFile The name of the style file to process.
         * @param coll A map collector to receive the created elements.

         */

        private StyledConverter makeStyleConverter(String styleFile, MapCollector coll) throws FileNotFoundException {
                Style style = new StyleImpl(styleFile, null);
                return new StyledConverter(style, coll, new EnhancedProperties());
        }

        /**
         * A special styled converted that attempts to produce the correct theoretical
         * result of running the style rules in order by literally doing that.
         * This should produce the same result as {@link #makeStyleConverter} and
         * can be used as a test of the strict style ordering branch.
         * @param styleFile The name of the style file to process.
         * @param coll A map collector to receive the created elements.
         */

        private StyledConverter makeStrictStyleConverter(String styleFile, MapCollector coll) throws FileNotFoundException {
                Style style = new ReferenceStyle(styleFile, null);
                return new StyledConverter(style, coll, new EnhancedProperties());
        }

        public static void forceUseOfGiven(boolean force) {
                forceUseOfGiven = force;
        }

        /**
         * This is a reference implementation of the style engine which is somewhat
         * independent of the main implementation and does not have any kind of
         * optimisations.  You can compare the results from the two implementations
         * to find bugs and regressions.
         */

        private class ReferenceStyle extends StyleImpl {
                private final StyleFileLoader fileLoader;
                private LevelInfo[] levels;

                /**
                 * Create a style from the given location and name.
                 *
                 * @param loc The location of the style. Can be null to mean just check the
                 * classpath.
                 * @param name The name.  Can be null if the location isn't.  If it is null
                 * then we just check for the first version file that can be found.
                 * @throws FileNotFoundException If the file doesn't exist.  This can include
                 * the version file being missing.
                 */

                public ReferenceStyle(String loc, String name) throws FileNotFoundException {
                        super(loc, name);
                        fileLoader = StyleFileLoader.createStyleLoader(loc, name);

                        setupReader();
                        readGivenResults();
                }

                private void setupReader() {
                        String l = LevelInfo.DEFAULT_LEVELS;
                        levels = LevelInfo.createFromString(l);
                }

                private void readGivenResults() {
                        givenResults.clear();
                        BufferedReader br = null;
                        try {
                                Reader reader = fileLoader.open("results");
                                br = new BufferedReader(reader);
                                String line;
                                while ((line = br.readLine()) != null) {
                                        line = line.trim();
                                        if (line.isEmpty())
                                                continue;
                                        givenResults.add(line);
                                }
                        } catch (IOException e) {
                                // there are no known good results given, that is OK
                        } finally {
                                Utils.closeFile(br);
                        }
                }

                /**
                 * Throws away the rules as previously read and reads again using the
                 * SimpleRuleFileReader which does not re-order or optimise the rules
                 * in any way.
                 *
                 * @return A simple list of rules with a resolving method that applies
                 * each rule in turn to the element until there is match.
                 */

                public Rule getWayRules() {
                        ReferenceRuleSet r = new ReferenceRuleSet();
                        r.addAll((ReferenceRuleSet) getLineRules());
                        r.addAll((ReferenceRuleSet) getPolygonRules());
                        return r;
                }

                /**
                 * Throws away the existing rules for the lines and re-reads them using
                 * the SimpleRuleFileReader that does not re-order or optimise the rules in any
                 * way.
                 *
                 * @return A Reference rule set of the lines.
                 */

                public Rule getLineRules() {
                        ReferenceRuleSet r = new ReferenceRuleSet();

                        SimpleRuleFileReader ruleFileReader = new SimpleRuleFileReader(FeatureKind.POLYLINE, levels, r);
                        try {
                                ruleFileReader.load(fileLoader, "lines");
                        } catch (FileNotFoundException e) {
                                e.printStackTrace();
                        }

                        return r;
                }

                /**
                 * Throws away the existing rules for the polygons and re-reads them using
                 * the SimpleRuleFileReader that does not re-order or optimise the rules in any
                 * way.
                 *
                 * @return A Reference rule set of the polygons.
                 */

                public Rule getPolygonRules() {
                        ReferenceRuleSet r = new ReferenceRuleSet();

                        SimpleRuleFileReader ruleFileReader = new SimpleRuleFileReader(FeatureKind.POLYGON, levels, r);
                        try {
                                ruleFileReader.load(fileLoader, "polygons");
                        } catch (FileNotFoundException e) {
                                // not a problem
                        }

                        return r;
                }
               
                public Rule getRelationRules() {
                        ReferenceRuleSet r = new ReferenceRuleSet();

                        SimpleRuleFileReader ruleFileReader = new SimpleRuleFileReader(FeatureKind.RELATION, levels, r);
                        try {
                                ruleFileReader.load(fileLoader, "relations");
                        } catch (FileNotFoundException e) {
                                // its not a problem
                        }

                        return r;
                }

                public Set<String> getUsedTags() {
                        return null;
                }

                /**
                 * Keeps each rule in an ordered list.
                 *
                 * Types are resolved by literally applying the rules in order to the
                 * element.
                 *
                 * As long as the rules are added in the order they are encountered in
                 * the file, this should work.
                 */

                private class ReferenceRuleSet implements Rule {
                        private final List<Rule> rules = new ArrayList<Rule>();
                        int cacheId = 0;
                       
                        public void add(Rule rule) {
                                rules.add(rule);
                        }

                        public void addAll(ReferenceRuleSet rs) {
                                for (Rule r : rs.rules) {
                                        add(r);
                                }
                        }

                        public void resolveType(Element el, TypeResult result) {
                                String tagsBefore = el.toTagString();
                                if (showMatches) {
                                        out.println("# Tags before: " + tagsBefore);
                                }
                                WatchableTypeResult a = new WatchableTypeResult(result);
                                // Start by literally running through the rules in order.
                                for (Rule rule : rules) {
                                        a.reset();
                                        cacheId = rule.resolveType(cacheId, el, a);
                                       
                                        if (showMatches) {
                                                if (a.isFound()) {
                                                        out.println("# Matched: " + rule);
                                                } else if (a.isActionsOnly())
                                                        out.println("# Matched for actions: " + rule);
                                        }

                                        if (a.isResolved())
                                                break;
                                }
                                if (showMatches && !tagsBefore.equals(el.toTagString()))
                                        out.println("# Way tags after: " + el.toTagString());
                        }

                        @Override
                        public int resolveType(int cacheId, Element el, TypeResult result) {
                                resolveType(el, result);
                                return cacheId;
                        }


                        public void setFinalizeRule(Rule finalizeRule) {
                                for (Rule rule : rules) {
                                        rule.setFinalizeRule(finalizeRule);
                                }
                        }

                        @Override
                        public void printStats(String header) {
                                // TODO Auto-generated method stub
                        }
                }

                /**
                 * A reimplementation of RuleFileReader that does no optimisation but
                 * just reads the rules into a list.
                 *
                 * Again this can be compared with the main implementation which may
                 * attempt more optimisations.
                 */

                class SimpleRuleFileReader {
                        private final TypeReader typeReader;

                        private final ReferenceRuleSet rules;
                        private ReferenceRuleSet finalizeRules;
                        private TokenScanner scanner;
                        private boolean inFinalizeSection = false;

                        public SimpleRuleFileReader(FeatureKind kind, LevelInfo[] levels, ReferenceRuleSet rules) {
                                this.rules = rules;
                                typeReader = new TypeReader(kind, levels);
                        }

                        /**
                         * Read a rules file.
                         * @param loader A file loader.
                         * @param name The name of the file to open.
                         * @throws FileNotFoundException If the given file does not exist.
                         */

                        public void load(StyleFileLoader loader, String name) throws FileNotFoundException {
                                Reader r = loader.open(name);
                                load(r, name);
                        }

                        void load(Reader r, String name) {
                                scanner = new TokenScanner(name, r);
                                scanner.setExtraWordChars("-:");

                                ExpressionReader expressionReader = new ExpressionReader(scanner, FeatureKind.POLYLINE);
                                ActionReader actionReader = new ActionReader(scanner);

                                // Read all the rules in the file.
                                scanner.skipSpace();
                                while (!scanner.isEndOfFile()) {
                                        if (checkCommand(scanner))
                                                continue;
                                       
                                        Op expr = expressionReader.readConditions();

                                        ActionList actions = actionReader.readActions();

                                        // If there is an action list, then we don't need a type
                                        GType type = null;
                                        if (scanner.checkToken("["))
                                                type = typeReader.readType(scanner);
                                        else if (actions == null)
                                                throw new SyntaxException(scanner, "No type definition given");

                                        saveRule(expr, actions, type);
                                        scanner.skipSpace();
                                }
                                if (finalizeRules != null) {
                                        rules.setFinalizeRule(finalizeRules);
                                }
                        }

                        private boolean checkCommand(TokenScanner scanner) {
                                scanner.skipSpace();
                                if (scanner.isEndOfFile())
                                        return false;

                                if (inFinalizeSection == false && scanner.checkToken("<")) {
                                        Token token = scanner.nextToken();
                                        if (scanner.checkToken("finalize")) {
                                                Token finalizeToken = scanner.nextToken();
                                                if (scanner.checkToken(">")) {
                                                        // consume the > token
                                                        scanner.nextToken();
                                                        // mark start of the finalize block
                                                        inFinalizeSection = true;
                                                        finalizeRules = new ReferenceRuleSet();
                                                        return true;
                                                } else {
                                                        scanner.pushToken(finalizeToken);
                                                        scanner.pushToken(token);
                                                }
                                        } else {
                                                scanner.pushToken(token);
                                        }
                                }
                                scanner.skipSpace();
                                return false;
                        }
                       
                        /**
                         * Save the expression as a rule.
                         */

                        private void saveRule(Op op, ActionList actions, GType gt) {
                                Rule rule;
                                if (actions.isEmpty())
                                        rule = new ExpressionRule(op, gt);
                                else
                                        rule = new ActionRule(op, actions.getList(), gt);

                                if (inFinalizeSection)
                                        finalizeRules.add(rule);
                                else
                                        rules.add(rule);
                        }
                }
        }

        /**
         * A map collector that just adds any line or road we find to the end of
         * a list.
         */

        private static class LocalMapCollector implements MapCollector {
                private final List<MapElement> lines;

                private LocalMapCollector(List<MapElement> lines) {
                        this.lines = lines;
                }

                public void addToBounds(Coord p) { }

                // could save points in the same way as lines to test them
                public void addPoint(MapPoint point) { }

                public void addLine(MapLine line) {
                        lines.add(line);
                }

                public void addShape(MapShape shape) { }

                public void addRoad(MapRoad road) {
                        lines.add(road);
                }

                public int addRestriction(GeneralRouteRestriction grr) {
                        return 0;
                }

                public void addThroughRoute(int junctionNodeId, long roadIdA, long roadIdB) {
                }
        }

        /**
         * A map collector that just prints elements found.
         * (lines and roads only at present).
         */

        private static class PrintingMapCollector implements MapCollector {
                private long start;

                public void addToBounds(Coord p) { if (start == 0) {
                                System.err.println("start collection");
                                start = System.currentTimeMillis();
                        }}

                // could save points in the same way as lines to test them
                public void addPoint(MapPoint point) { }

                public void addLine(MapLine line) {
                        if (start == 0) {
                                System.err.println("start collection");
                                start = System.currentTimeMillis();
                        }
                        if (print) {
                                String[] strings = formatResults("", Arrays.<MapElement>asList(line));
                                printResult(strings);
                        }
                }

                public void addShape(MapShape shape) { }

                public void addRoad(MapRoad road) {
                        if (print) {
                                String[] strings = formatResults("", Collections.<MapElement>singletonList(road));
                                printResult(strings);
                        }
                }

                public int addRestriction(GeneralRouteRestriction grr) {
                        return 0;
                }

                public void addThroughRoute(int junctionNodeId, long roadIdA, long roadIdB) {
                }

                public long getStart() {
                        return start;
                }
        }
}