Subversion Repositories mkgmap

Rev

Rev 4715 | 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.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
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 org.xml.sax.SAXException;

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.OsmXmlHandler;
import uk.me.parabola.mkgmap.reader.osm.xml.OsmXmlHandler.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;


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

        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<>();
                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);
                                OsmXmlHandler handler = new OsmXmlHandler();
                                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) {
                // 14Jan20 Changed from using DefaultCharset to UTF-8      
                try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filename), StandardCharsets.UTF_8))) {
                        List<Way> ways = readSimpleTestFile(br);

                        List<String> givenList = readGivenResults();
                        boolean noStrict = false;
                        if (!givenList.isEmpty() && Objects.equals(givenList.get(0), "NO-STRICT")) {
                                givenList.remove(0);
                                noStrict = true;
                        }

                        List<MapElement> strictResults = new ArrayList<>();
                        List<MapElement> results = new ArrayList<>();
                        List<String> actual = new ArrayList<>();
                        List<String> expected = new ArrayList<>();
                        for (Way w : ways) {
                                OsmConverter normal = new StyleTester(STYLETESTER_STYLE, new LocalMapCollector(results), false);

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

                                if (!noStrict) {
                                        OsmConverter strict = new StyleTester(STYLETESTER_STYLE, new LocalMapCollector(strictResults), true);
                                        strict.convertWay(w.copy());
                                        strict.end();
                                        expected.addAll(formatResults(prefix, strictResults));
                                        strictResults.clear();
                                }
                        }

                        printResult("", actual);
                        if (!noStrict && !Objects.equals(actual, expected)) {
                                out.println("ERROR expected result is:");
                                printResult("|", expected);
                        }

                        if ((!givenList.isEmpty() || forceUseOfGiven) && !Objects.equals(actual, givenList)) {
                                out.println("ERROR given results were:");
                                printResult("", givenList);
                        }
                } catch (FileNotFoundException e) {
                        System.err.println("Cannot open test file " + filename);
                } catch (IOException e) {
                        System.err.println("Failure while reading test file " + filename);
                }
        }

        private static List<String> readGivenResults() {
                List<String> givenResults = new ArrayList<>();

                //BufferedReader br = null;
                try (StyleFileLoader fileLoader = StyleFileLoader.createStyleLoader(STYLETESTER_STYLE, null);
                        Reader reader = fileLoader.open("results");
                        BufferedReader 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
                }

                return givenResults;
        }

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

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

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

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

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

        private static void printResult(String prefix, List<String> results) {
                for (String s : results) {
                        out.println(prefix + 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<>();

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

                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 List<String> formatResults(String prefix, List<MapElement> lines) {
                List<String> result = new ArrayList<>();
                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.add(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) {
                StringBuilder sb = new StringBuilder();
                sb.append(String.format("Line 0x%x, labels=%s, res=%d-%d",
                                el.getType(), Arrays.toString(el.getLabels()),
                                el.getMinResolution(), el.getMaxResolution()));
                if (el.isDirection())
                        sb.append(" oneway");

                sb.append(' ');
                for (Coord co : el.getPoints())
                        sb.append(String.format("(%s),", co.toGarminString()));

                return sb.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) {
                StringBuilder sb = new StringBuilder(lineToString(el));
                sb.replace(0, 4, "Road");
                sb.append(String.format(" road class=%d speed=%d", el.getRoadDef().getRoadClass(),
                                getRoadSpeed(el.getRoadDef())));
                return sb.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 | 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 static 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() {
                forceUseOfGiven = true;
        }

        /**
         * 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.
                 */

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

                        setupReader();
                }

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

                /**
                 * 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.
                 */

                @Override
                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.
                 */

                @Override
                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.
                 */

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

                @Override
                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<>();
                        int cacheId;
                       
                        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 && !Objects.equals(tagsBefore, 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 Rule getFinalizeRule() {
                                if (rules.isEmpty())
                                        return null;
                                return rules.get(0).getFinalizeRule();
                        }

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

                        @Override
                        public boolean containsExpression(String exp) {
                                if (rules.isEmpty()) {
                                        // this method must be called after prepare() is called so
                                        // that we have rules to which the finalize rules can be applied
                                        throw new IllegalStateException("First call prepare() before setting the finalize rules");
                                }
                                for (Rule rule : rules){
                                        if (rule.containsExpression(exp))
                                                return true;
                                }
                                return getFinalizeRule()!= null && getFinalizeRule().containsExpression(exp);
                        }
                }

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

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

        /**
         * 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) {
                                List<String> strings = formatResults("", Collections.singletonList(line));
                                printResult("", strings);
                        }
                }

                public void addShape(MapShape shape) { }

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

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

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