Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2006 Steve Ratcliffe
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *
 * Author: Steve Ratcliffe
 * Create date: 16-Dec-2006
 */

package uk.me.parabola.mkgmap.reader.polish;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import uk.me.parabola.imgfmt.FormatException;
import uk.me.parabola.imgfmt.MapFailedException;
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.AccessTagsAndBits;
import uk.me.parabola.imgfmt.app.net.NumberStyle;
import uk.me.parabola.imgfmt.app.net.Numbers;
import uk.me.parabola.imgfmt.app.trergn.ExtTypeAttributes;
import uk.me.parabola.imgfmt.app.trergn.TREHeader;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.filters.LineSplitterFilter;
import uk.me.parabola.mkgmap.general.CityInfo;
import uk.me.parabola.mkgmap.general.LevelInfo;
import uk.me.parabola.mkgmap.general.LoadableMapDataSource;
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.general.ZipCodeInfo;
import uk.me.parabola.mkgmap.reader.MapperBasedMapDataSource;
import uk.me.parabola.mkgmap.reader.osm.FakeIdGenerator;
import uk.me.parabola.mkgmap.reader.osm.FeatureKind;
import uk.me.parabola.mkgmap.reader.osm.GType;
import uk.me.parabola.mkgmap.reader.osm.GeneralRelation;
import uk.me.parabola.mkgmap.reader.osm.MultiPolygonRelation;
import uk.me.parabola.mkgmap.reader.osm.Way;

/**
 * Read an data file in Polish format.  This is the format used by a number
 * of other garmin map making programs notably cGPSmapper.
 * <p>
 * As the input format is designed for garmin maps, it is fairly easy to read
 * into mkgmap.  Not every feature of the format is read yet, but it shouldn't
 * be too difficult to add them in as needed.
 * <p>
 * Now will place elements at the level specified in the file and not at the
 * automatic level that is used in eg. the OSM reader.
 */

public class PolishMapDataSource extends MapperBasedMapDataSource implements LoadableMapDataSource {
        private static final Logger log = Logger.getLogger(PolishMapDataSource.class);

        private static final Charset READING_CHARSET = StandardCharsets.ISO_8859_1;

        private static final int S_IMG_ID = 1;
        private static final int S_POINT = 2;
        private static final int S_POLYLINE = 3;
        private static final int S_POLYGON = 4;
    private static final int S_RESTRICTION = 5;

        private MapPoint point;
        private MapLine polyline;
        private MapShape shape;

    private PolishTurnRestriction restriction;

    /** contains the information from the lines starting with DATA */
        private final Map<Integer, List<List<Coord>>> lineStringMap = new LinkedHashMap<>();

        private final RoadHelper roadHelper = new RoadHelper();
    private final RestrictionHelper restrictionHelper = new RestrictionHelper();

        private Map<String, String> extraAttributes;

        private String copyright;
        private int section;
        private LevelInfo[] levels;
        private int endLevel;
        private char elevUnits;
        private int currentLevel;
        private boolean dataHighLevel;
        private boolean background;
        private int poiDispFlag;
        private String defaultCountry;
        private String defaultRegion;
        private static final double METERS_TO_FEET = 3.2808399;

        private int lineNo;

        private boolean havePolygon4B;

        /** if false, assume that lines with routable types are roads and create corresponding NET data */
        private boolean routing;
        private int roadIdGenerated;

        // Use to decode labels if they are not in cp1252
        private CharsetDecoder dec;

        public boolean isFileSupported(String name) {
                // Supported if the extension is .mp
                return name.endsWith(".mp") || name.endsWith(".MP") || name.endsWith(".mp.gz");
        }

        @Override
        public void load(String name, boolean addBackground) throws FileNotFoundException {
                // If no code page is given then we read labels in utf-8
                dec = StandardCharsets.UTF_8.newDecoder();
                dec.onUnmappableCharacter(CodingErrorAction.REPLACE);

                try (BufferedReader in = new BufferedReader(new InputStreamReader(Utils.openFile(name), READING_CHARSET))) {
                        String line;
                        while ((line = in.readLine()) != null) {
                                ++lineNo;
                                line = line.trim();
                                if (line.isEmpty() || line.charAt(0) == ';')
                                        continue;
                                if (line.toUpperCase().startsWith("[END"))
                                        endSection();
                                else if (line.charAt(0) == '[')
                                        sectionStart(line);
                                else
                                        processLine(line);
                        }
                } catch (IOException e) {
                        throw new FormatException("Reading file failed", e);
                }

                // Add all restrictions to the map after reading the full map.
                // The reason being, the restrictions section appear in the beginning of the map.
                // All the nodes will only be read later on.
                // Required to pass the road helper instance as it contains all node data.
                restrictionHelper.processAndAddRestrictions(roadHelper, mapper);

                if (addBackground && !havePolygon4B)
                        addBackground();
        }

        public LevelInfo[] mapLevels() {
                if (levels == null) {
                        // If it has not been set then supply some defaults.
                        levels = new LevelInfo[] {
                                        new LevelInfo(3, 17),
                                        new LevelInfo(2, 18),
                                        new LevelInfo(1, 22),
                                        new LevelInfo(0, 24),
                        };
                }
                levels[0].setTop(true);
                return levels;
        }

       
        @Override
        public LevelInfo[] overviewMapLevels() {
                String levelSpec = getConfig().getProperty("overview-levels");
               
                if (levelSpec == null)
                        return null;
                LevelInfo[] ovLevels = LevelInfo.createFromString(levelSpec);
                for (int i = 0; i < ovLevels.length; i++) {
                        ovLevels[i] = new LevelInfo(ovLevels.length - i - 1, ovLevels[i].getBits());
                }
                return ovLevels;
        }

        /**
         * Get the copyright message.  
         *
         * @return A string description of the copyright.
         */

        public String[] copyrightMessages() {
                String copyrightFileName = getConfig().getProperty("copyright-file", null);
                if (copyrightFileName != null) {
                        return readCopyrightFile(copyrightFileName);
                }
                if (copyright == null) {
                        copyright = getConfig().getProperty("copyright-message", null);
                }
                return new String[] { copyright };
        }

        /**
         * Record that we are starting a new section.
         * Section names are enclosed in square brackets.  Inside the section there
         * are a number of lines with the key=value format.
         *
         * @param line The raw line from the input file.
         */

        private void sectionStart(String line) {
                String name = line.substring(1, line.length() - 1).trim();
                log.debug("section name", name);

                extraAttributes = null;

                if ("IMG ID".equalsIgnoreCase(name)) {
                        section = S_IMG_ID;
                        poiDispFlag = 0;
                } else if ("POI".equalsIgnoreCase(name) || "RGN10".equals(name) || "RGN20".equals(name)) {
                        point = new MapPoint();
                        section = S_POINT;
                } else if ("POLYLINE".equalsIgnoreCase(name) || "RGN40".equals(name)) {
                        polyline = new MapLine();
                        roadHelper.clear();
                        section = S_POLYLINE;
                } else if ("POLYGON".equalsIgnoreCase(name) || "RGN80".equals(name)) {
                        shape = new MapShape();
                        section = S_POLYGON;
                } else if ("Restrict".equalsIgnoreCase(name)) {
            restriction = new PolishTurnRestriction();
            section = S_RESTRICTION;
        }
                else
                        log.info("Ignoring unrecognised section: " + name);
        }

        /**
         * At the end of a section, we add what ever element that we have been
         * building to the map.
         */

        private void endSection() {
                switch (section) {

                case S_IMG_ID:
                        break;

                case S_POINT:
                        if (point.getLocation() != null) {
                                if(extraAttributes != null && point.hasExtendedType())
                                        point.setExtTypeAttributes(makeExtTypeAttributes());
                                mapper.addToBounds(point.getLocation());
                                mapper.addPoint(point);
                        } else {
                                log.error("skipping POI without coordinates near line " + lineNo );
                        }
                               
                        break;
                case S_POLYLINE:
                        if (!lineStringMap.isEmpty()) {
                                MapLine origPolyline = polyline.copy();
                               
                                for (Map.Entry<Integer , List<List<Coord>>> entry : lineStringMap.entrySet()) {
                                        int level = entry.getKey();
                                        setResolution(origPolyline, level);
                                        for (List<Coord> points : entry.getValue()) {
                                                polyline = origPolyline.copy();
                                                if (!routing && GType.isSpecialRoutableLineType(polyline.getType())) {
                                                        roadHelper.setRoadId(++roadIdGenerated);
                                                }
                                                if (roadHelper.isRoad() && level == 0) {
                                                        polyline.setPoints(points);
                                                        MapRoad r = roadHelper.makeRoad(polyline);
                                                        if (!routing) {
                                                                r.skipAddToNOD(true);
                                                        }
                                                        mapper.addRoad(r);
                                                }
                                                else {
                                                        if(extraAttributes != null && polyline.hasExtendedType())
                                                                polyline.setExtTypeAttributes(makeExtTypeAttributes());
                                                        final int maxPointsInLine = LineSplitterFilter.MAX_POINTS_IN_LINE;
                                                        if(points.size() > maxPointsInLine) {
                                                                List<Coord> segPoints = new ArrayList<>(maxPointsInLine);
                                                                for(Coord p : points) {
                                                                        segPoints.add(p);
                                                                        if(segPoints.size() == maxPointsInLine) {
                                                                                MapLine seg = polyline.copy();
                                                                                seg.setPoints(segPoints);
                                                                                mapper.addLine(seg);
                                                                                segPoints = new ArrayList<>(maxPointsInLine);
                                                                                segPoints.add(p);
                                                                        }
                                                                }
                                                                if(!segPoints.isEmpty()) {
                                                                        polyline.setPoints(segPoints);
                                                                        mapper.addLine(polyline);
                                                                }
                                                        }
                                                        else {
                                                                polyline.setPoints(points);
                                                                mapper.addLine(polyline);
                                                        }
                                                }
                                        }
                                }
                        }
                        break;
                case S_POLYGON:
                        if (!lineStringMap.isEmpty()) {
                                if (extraAttributes != null && shape.hasExtendedType())
                                        shape.setExtTypeAttributes(makeExtTypeAttributes());
                                if (background && !dataHighLevel)
                                        endLevel = levels.length -1;
                                for (Map.Entry<Integer , List<List<Coord>>> entry : lineStringMap.entrySet()) {
                                        setResolution(shape, entry.getKey());
                                        addShapesFromPattern(entry.getValue());
                                }
                        }
                        break;
        case S_RESTRICTION:
            restrictionHelper.addRestriction(restriction);
            break;
                case 0:
                        // ignored section
                        break;

                default:
                        log.warn("unexpected default in switch", section);
                        break;
                }

                // Clear the section state.
                section = 0;
                endLevel = 0;
                lineStringMap.clear();
                currentLevel = 0;
                dataHighLevel = false;
                background = false;
        }

        private void addShapesFromPattern(List<List<Coord>> pointsLists) {
                if (pointsLists.size() == 1) {
                        MapShape copy = shape.copy();
                        copy.setPoints(pointsLists.get(0));
                        mapper.addShape(copy);
                } else {
                        // we have a multipolygon with multiple rings, use
                        // MultiPolygonRelation to compute the geometry
                        Map<Long, Way> wayMap = new HashMap<>();
                        GeneralRelation gr = new GeneralRelation(FakeIdGenerator.makeFakeId());
                        // add one tag so that MultiPolygonRelation doesn't ignore the relation
                        gr.addTag("code", Integer.toHexString(shape.getType()));
                        for (int i = 0; i < pointsLists.size(); i++) {
                                Way w = new Way(FakeIdGenerator.makeFakeId(), pointsLists.get(i));
                                wayMap.put(w.getId(), w);
                                // empty role, let MultiPolygonRelation find out what is inner or outer
                                gr.addElement("", w);  
                        }
                        MultiPolygonRelation mp = new MultiPolygonRelation(gr, wayMap, Area.PLANET);
                        mp.processElements();
                        for (Way s: wayMap.values()) {
                                if (MultiPolygonRelation.STYLE_FILTER_POLYGON.equals(s.getTag(MultiPolygonRelation.STYLE_FILTER_TAG))) {
                                        MapShape copy = shape.copy();
                                        copy.setPoints(s.getPoints());
                                        mapper.addShape(copy);
                                }
                        }
                }
               
        }

        /**
         * This should be a line that is a key value pair.  We switch out to a
         * routine that is dependent on the section that we are in.
         *
         * @param line The raw input line from the file.
         */

        private void processLine(String line) {
                String[] nameVal = line.split("=", 2);
                if (nameVal.length != 2) {
                        log.warn("short line? " + line);
                        return;
                }
                String name = nameVal[0].trim();
                String value = nameVal[1].trim();

                log.debug("LINE: ", name, "|", value);
               
                switch (section) {
                case S_IMG_ID:
                        imgId(name, value);
                        break;
                case S_POINT:
                        if (!isCommonValue(point, name, value))
                                point(name, value);
                        break;
                case S_POLYLINE:
                        if (!isCommonValue(polyline, name, value))
                                line(name, value);
                        break;
                case S_POLYGON:
                        if (!isCommonValue(shape, name, value))
                                shape(name, value);
                        break;
        case S_RESTRICTION:
            restriction(name, value);
            break;
                default:
                        log.debug("line ignored");
                        break;
                }
        }


        /**
         * This is called for every line within the POI section.  The lines are
         * key value pairs that have already been decoded into name and value.
         * For each name we recognise we set the appropriate property on
         * the <i>point</i>.
         *
         * @param name Parameter name.
         * @param value Its value.
         */

        private void point(String name, String value) {
                if ("Type".equals(name)) {
                        int type = Integer.decode(value);
                        if (type <= 0xff)
                                type <<= 8;
                        point.setType(type);
                        checkType(FeatureKind.POINT, point.getType());
                } else if ("SubType".equals(name)) {
                        int subtype = Integer.decode(value);
                        int type = point.getType();
                        point.setType(type | subtype);
                        checkType(FeatureKind.POINT, point.getType());
                } else if (name.startsWith("Data") || name.startsWith("Origin")) {
                        Coord co = makeCoord(value);
                        setResolution(point, name);
                        point.setLocation(co);
                }
                else {
                        if(extraAttributes == null)
                                extraAttributes = new HashMap<>();
                        extraAttributes.put(name, value);
                }
        }

        /**
         * Called for each command in a POLYLINE section.  There will be a Data
         * line consisting of a number of co-ordinates that must be separated out
         * into points.
         *
         * @param name Command name.
         * @param value Command value.
         * @see #point
         */

        private void line(String name, String value) {
                if ("Type".equals(name)) {
                        polyline.setType(decodeType(FeatureKind.POLYLINE, value));
                } else if (name.startsWith("Data")) {
                        extractResolution(name);
                        addLineString(value, false);
                        // If it is a contour line, then fix the elevation if required.
                        if (GType.isContourLine(polyline)) {
                                fixElevation();
                        }
                } else if ("RoadID".equals(name)) {
                        if (!routing && roadIdGenerated > 0)
                                throw new MapFailedException("found RoadID without Routing=Y in [IMG ID] section in line " + lineNo);
                        roadHelper.setRoadId(Integer.parseInt(value));
                } else if (name.startsWith("Nod")) {
                        roadHelper.addNode(value);
                } else if ("RouteParam".equals(name) || "RouteParams".equals(name)) {
                        roadHelper.setParam(value);
                } else if ("DirIndicator".equals(name)) {
                        polyline.setDirection(Integer.parseInt(value) > 0);
                } else if (name.startsWith("Numbers")) {
                        roadHelper.addNumbers(parseNumbers(value));
                } else {
                        if (extraAttributes == null)
                                extraAttributes = new HashMap<>();
                        extraAttributes.put(name, value);
                }
        }

        /**
         * This constructor takes a comma separated list as in the polish format. Also used in testing as
         * it is an easy way to set all common parameters at once.
         *
         * @param spec Node number, followed by left and then right parameters as in the polish format.
         */

        public Numbers parseNumbers(String spec) {
                Numbers nums = new Numbers();
                String[] strings = spec.split(",");
                nums.setPolishIndex(Integer.parseInt(strings[0]));
                NumberStyle numberStyle = NumberStyle.fromChar(strings[1]);
                int start = Integer.parseInt(strings[2]);
                int end = Integer.parseInt(strings[3]);
                nums.setNumbers(Numbers.LEFT, numberStyle, start, end);
                numberStyle = NumberStyle.fromChar(strings[4]);
                start = Integer.parseInt(strings[5]);
                end = Integer.parseInt(strings[6]);
                nums.setNumbers(Numbers.RIGHT, numberStyle, start, end);

                if (strings.length > 8){
                        // zip codes
                        String zip = strings[7];
                        if (!"-1".equals(zip))
                                nums.setZipCode(Numbers.LEFT, new ZipCodeInfo(zip));
                        zip = strings[8];
                        if (!"-1".equals(zip))
                                nums.setZipCode(Numbers.RIGHT, new ZipCodeInfo(zip));
                }
                if (strings.length > 9){
                        String city,region,country;
                        int nextPos = 9;
                        city = strings[nextPos];
                        if (!"-1".equals(city)){
                                region = strings[nextPos + 1];
                                country = strings[nextPos + 2];
                                nums.setCityInfo(Numbers.LEFT, createCityInfo(city, region, country));
                                nextPos = 12;
                        } else {
                                nextPos = 10;
                        }
                        city = strings[nextPos];
                        if (!"-1".equals(city)){
                                region = strings[nextPos + 1];
                                country = strings[nextPos + 2];
                                nums.setCityInfo(Numbers.RIGHT, createCityInfo(city, region, country));
                        }
                }
                return nums;
        }
       
        private CityInfo createCityInfo(String city, String region, String country) {
                return new CityInfo(recode(city), recode(region), unescape(recode(country)));
        }
       
        private List<Coord> coordsFromString(String value, boolean close) {
                String[] ords = value.split("\\) *, *\\(");
                List<Coord> points = new ArrayList<>();

                for (String s : ords) {
                        Coord co = makeCoord(s);
                        if (log.isDebugEnabled())
                                log.debug(" L: ", co);
                        mapper.addToBounds(co);
                        points.add(co);
                }
                if (close && points.get(0) != points.get(points.size() - 1)) {
                        // not closed, close it
                        if (points.get(0).highPrecEquals(points.get(points.size() - 1))) {
                                points.set(points.size() - 1, points.get(0));
                        } else {
                                points.add(points.get(0));
                        }
                }
                log.debug(points.size() + " points from " + value);
                return points;
        }

        /**
         * The elevation needs to be in feet.  So if it is given in meters then
         * convert it.
         */

        private void fixElevation() {
                if (elevUnits == 'm') {
                        String h = polyline.getName();
                        if (h == null)
                                return;
                        try {
                                // Convert to feet.
                                double m = Double.parseDouble(h);
                                int feet = (int) Math.round(m * METERS_TO_FEET);
                                polyline.setName(Integer.toString(feet));

                        } catch (NumberFormatException e) {
                                log.warn(h + " doesn't seem to be a valid elevation value in m" );
                        }
                }
        }

        /**
         * Called for each command in a POLYGON section.  There will be a Data
         * line consisting of a number of co-ordinates that must be separated out
         * into points.
         *
         * @param name Command name.
         * @param value Command value.
         * @see #line
         */

        private void shape(String name, String value) {
                if ("Type".equals(name)) {
                        shape.setType(decodeType(FeatureKind.POLYGON, value));
                        if(shape.getType() == 0x4b)
                                havePolygon4B = true;
                } else if (name.startsWith("Data")) {
                        extractResolution(name);
                        addLineString(value, true);
                } else if ("Background".equals(name)) {
                        if ("Y".equals(value))
                                background = true;
                }
                else {
                        if(extraAttributes == null)
                                extraAttributes = new HashMap<>();
                        extraAttributes.put(name, value);
                }
        }

        private void addLineString (String value, boolean close) {
                lineStringMap.computeIfAbsent(currentLevel, k -> new ArrayList<>()).add(coordsFromString(value, close));
        }
       
        private boolean isCommonValue(MapElement elem, String name, String value) {
                if ("Label".equals(name)) {
                        elem.setName(unescape(recode(value)));
                } else if ("Label2".equals(name) || "Label3".equals(name)) {
                        elem.add2Name(unescape(recode(value)));
                } else if ("Levels".equals(name) || "EndLevel".equals(name) || "LevelsNumber".equals(name)) {
                        try {
                                endLevel = Integer.valueOf(value);
                        } catch (NumberFormatException e) {
                                endLevel = 0;
                        }
                } else if ("ZipCode".equals(name)) {
                  elem.setZip(recode(value));
                } else if ("CityName".equals(name)) {
                  elem.setCity(recode(value));           
                } else if ("StreetDesc".equals(name)) {
                  elem.setStreet(recode(value));
                } else if ("HouseNumber".equals(name)) {
                  elem.setHouseNumber(recode(value));
                } else if ("is_in".equals(name)) {
                  elem.setIsIn(recode(value));           
                } else if ("Phone".equals(name)) {
                  elem.setPhone(recode(value));                
                } else if ("CountryName".equals(name)) {
                  elem.setCountry(unescape(recode(value)));
                } else if ("RegionName".equals(name)) {
                  elem.setRegion(recode(value));                               
                } else {
                        return false;
                }

                // We dealt with it
                return true;
        }

        /**
         * Deal with the polish map escape codes of the form ~[0x##].  These
         * stand for a single character and is usually used for highway
         * symbols, name separators etc.
         *
         * The code ~[0x05] stands for the character \005 for example.
         *
         * @param s The original string that may contain codes.
         * @return A string with the escape codes replaced by the single character.
         */

        public static String unescape(String s) {
                int ind = s.indexOf("~[");
                if (ind < 0)
                        return s;

                StringBuilder sb = new StringBuilder();
                if (ind > 0)
                        sb.append(s.substring(0, ind));

                char[] buf = s.toCharArray();
                while (ind < buf.length) {
                        if (ind < buf.length-2 && buf[ind] == '~' && buf[ind+1] == '[') {
                                StringBuilder num = new StringBuilder();
                                ind += 2; // skip "~["
                                while (ind < buf.length && buf[ind++] != ']')
                                        num.append(buf[ind - 1]);

                                try {
                                        int inum = Integer.decode(num.toString());

                                        // Convert any that are in 6-bit format
                                        if (inum == 0x1b2c) inum = 0x1c;
                                        if (inum >= 0x2a)
                                                inum -= 0x29;
                                        sb.append((char) inum);
                                } catch (NumberFormatException e) {
                                        // Input is malformed so lets just ignore it.
                                }
                        } else {
                                sb.append(buf[ind]);
                                ind++;
                        }
                }
                return sb.toString();
        }

        /**
         * Convert the value of a label into a string based on the declared
         * code page in the file.
         *
         * This makes assumptions about the way that the .mp file is written
         * that may not be correct.
         *
         * @param value The string that has been read with ISO-8859-1.
         * @return A possibly different string that is obtained by taking the
         * bytes in the input string and decoding them as if they had the
         * declared code page.
         */

        private String recode(String value) {
                if (dec != null) {
                        // Get the bytes that were actually in the file.
                        byte[] bytes = value.getBytes(READING_CHARSET);
                        ByteBuffer buf = ByteBuffer.wrap(bytes);

                        try {
                                // Decode from bytes with the correct code page.
                                CharBuffer out = dec.decode(buf);
                                return out.toString();
                        } catch (CharacterCodingException e) {
                                log.error("error decoding label", e);
                        }
                }
                return value;
        }

        private void setResolution(MapElement elem, String name) {
               
                if (endLevel > 0) {
                        elem.setMinResolution(extractResolution(endLevel));
                    elem.setMaxResolution(extractResolution(name));
                } else {
                        int res = extractResolution(name);
                        elem.setMinResolution(res);
                        elem.setMaxResolution(res);
                }
        }

        private void setResolution(MapElement elem, int level) {
                if (endLevel > 0) {
                    elem.setMaxResolution(extractResolution(level));
                        if (lineStringMap.size() > 1) {
                                for (int i = level+1; i < endLevel; i++) {
                                        if (lineStringMap.containsKey(i)) {
                                                elem.setMinResolution(extractResolution(i-1));
                                                return;
                                        }
                                }
                        }
                        elem.setMinResolution(extractResolution(endLevel));
                } else {
                        int res = extractResolution(level);
                        elem.setMinResolution(res);
                        elem.setMaxResolution(res);
                }
        }

        /**
         * Extract the resolution from the Data label.  The name will be something
         * like Data2: from that we know it is at level 2 and we can look up
         * the resolution.
         *
         * @param name The name tag DataN, where N is a digit corresponding to the
         * level.
         *
         * @return The resolution that corresponds to the level.
         */

        private int extractResolution(String name) {
                currentLevel = Integer.parseInt(name.substring(name.charAt(0) == 'O'? 6: 4));
                if (currentLevel > 0)
                        dataHighLevel = true;
                return extractResolution(currentLevel);
        }

        /**
         * Extract resolution from the level.
         *
         * @param level The level (0..)
         * @return The resolution.
         * @see #extractResolution(String name)
         */

        private int extractResolution(int level) {
                int nlevels = levels.length;

                // Some maps use EndLevel=9 to mean the highest level
                if (level >= nlevels)
                        level = nlevels - 1;

                LevelInfo li = levels[nlevels - level - 1];
                return li.getBits();
        }


        /**
         * The initial 'IMG ID' section.  Contains miscellaneous parameters for
         * the map.
         *
         * @param name Command name.
         * @param value Command value.
         */

        private void imgId(String name, String value) {
                if ("Copyright".equals(name)) {
                        copyright = value;
                } else if ("Levels".equals(name)) {
                        int nlev = Integer.parseInt(value);
                        levels = new LevelInfo[nlev];
                } else if (name.startsWith("Level")) {
                        int level = Integer.parseInt(name.substring(5), 10);
                        int bits = Integer.parseInt(value);
                        LevelInfo info = new LevelInfo(level, bits);

                        int nlevels = levels.length;
                        if (level >= nlevels)
                                return;

                        levels[nlevels - level - 1] = info;
                } else if (name.startsWith("Elevation")) {
                        char fc = value.charAt(0);
                        if (fc == 'm' || fc == 'M')
                                elevUnits = 'm';
                } else if ("CodePage".equalsIgnoreCase(name)) {
                        dec = Charset.forName("cp" + value).newDecoder();
                        dec.onUnmappableCharacter(CodingErrorAction.REPLACE);
                } else if (name.endsWith("LeftSideTraffic")){
                        if ("Y".equals(value)){
                                setDriveOnLeft(true);
                        } else if ("N".equals(value)){
                                setDriveOnLeft(false);
                        }
                } else if ("Transparent".equals(name)) {
                        if ("Y".equals(value) || "S".equals(value))
                                poiDispFlag |= TREHeader.POI_FLAG_TRANSPARENT;
                } else if ("POIZipFirst".equals(name)) {
                        if ("Y".equals(value))
                                poiDispFlag |= TREHeader.POI_FLAG_POSTALCODE_BEFORE_CITY;
                } else if ("POINumberFirst".equals(name)) {
                        if ("N".equals(value))
                                poiDispFlag |= TREHeader.POI_FLAG_STREET_BEFORE_HOUSENUMBER;  
                } else if ("Numbering".equals(name)) {
                        // ignore
                } else if ("Routing".equals(name)) {
                        if ("Y".equals(value)) {
                                routing = true; // don't generate roadid
                        }
                } else if ("CountryName".equalsIgnoreCase(name)) {
                        defaultCountry = value;
                } else if ("RegionName".equalsIgnoreCase(name)) {
                        defaultRegion = value;
                } else {
                        log.info("'IMG ID' section: ignoring " + name + " " + value);
                }
               
        }

        /**
         * Create a coordinate from a string.  The string will look similar:
         * (2.3454,-0.23), but may not have the leading opening parenthesis.
         * @param value A string representing a lat,long pair.
         * @return The coordinate value.
         */

        private static Coord makeCoord(String value) {
                String[] fields = value.split("[(,)]");

                int i = 0;
                if (fields[0].isEmpty())
                        i = 1;

                Double f1 = Double.valueOf(fields[i]);
                Double f2 = Double.valueOf(fields[i+1]);
                return new Coord(f1, f2);
        }

        private ExtTypeAttributes makeExtTypeAttributes() {
                Map<String, String> eta = new HashMap<>();
                int colour = 0;
                int style = 0;

                for(Map.Entry<String, String> entry : extraAttributes.entrySet()) {
                        String v = entry.getValue();
                        if ("Depth".equals(entry.getKey())) {
                                String u = extraAttributes.get("DepthUnit");
                                if("f".equals(u))
                                        v += "ft";
                                eta.put("depth", v);
                        } else if("Height".equals(entry.getKey())) {
                                String u = extraAttributes.get("HeightUnit");
                                if("f".equals(u))
                                        v += "ft";
                                eta.put("height", v);
                        } else if("HeightAboveFoundation".equals(entry.getKey())) {
                                String u = extraAttributes.get("HeightAboveFoundationUnit");
                                if("f".equals(u))
                                        v += "ft";
                                eta.put("height-above-foundation", v);
                        } else if("HeightAboveDatum".equals(entry.getKey())) {
                                String u = extraAttributes.get("HeightAboveDatumUnit");
                                if("f".equals(u))
                                        v += "ft";
                                eta.put("height-above-datum", v);
                        } else if("Color".equals(entry.getKey())) {
                                colour = Integer.decode(v);
                        } else if("Style".equals(entry.getKey())) {
                                style = Integer.decode(v);
                        } else if("Position".equals(entry.getKey())) {
                                eta.put("position", v);
                        } else if("FoundationColor".equals(entry.getKey())) {
                                eta.put("color", v);
                        } else if("Light".equals(entry.getKey())) {
                                eta.put("light", v);
                        } else if("LightType".equals(entry.getKey())) {
                                eta.put("type", v);
                        } else if("Period".equals(entry.getKey())) {
                                eta.put("period", v);
                        } else if("Note".equals(entry.getKey())) {
                                eta.put("note", v);
                        } else if("LocalDesignator".equals(entry.getKey())) {
                                eta.put("local-desig", v);
                        } else if("InternationalDesignator".equals(entry.getKey())) {
                                eta.put("int-desig", v);
                        } else if("FacilityPoint".equals(entry.getKey())) {
                                eta.put("facilities", v);
                        } else if("Racon".equals(entry.getKey())) {
                                eta.put("racon", v);
                        } else if("LeadingAngle".equals(entry.getKey())) {
                                eta.put("leading-angle", v);
                        }
                }

                if(colour != 0 || style != 0)
                        eta.put("style", "0x" + Integer.toHexString((style << 8) | colour));

                return new ExtTypeAttributes(eta, "Line " + lineNo);
        }

    /**
     * Construct the restrictions object.
     */

    private void restriction(String name, String value) {
        try {
            // Proceed only if the restriction is not already marked as invalid.
            if (restriction.isValid()) {
                if ("Nod".equalsIgnoreCase(name)) {
                    /* ignore */
                } else if ("TraffPoints".equalsIgnoreCase(name)) {
                        restriction.setTrafficPoints(value);
                } else if ("TraffRoads".equalsIgnoreCase(name)) {
                        restriction.setTrafficRoads(value);
                } else if ("RestrParam".equalsIgnoreCase(name)) {
                    restriction.setExceptMask(getRestrictionExceptionMask(value));
                } else if ("Time".equalsIgnoreCase(name)) {
                        log.info("Time in restriction definition is ignored " + restriction);
                }
            }
        } catch (NumberFormatException ex) { // This exception means that this restriction is not properly defined.
            restriction.setValid(false); // Mark this as an invalid restriction.
            log.error("Invalid restriction definition. " + restriction);
        }
    }

    /**
     * Constructs the vehicle exception mask from the restriction params.
     * From cGPSMapper manual :-
     * <p>
     * By default restrictions apply to all kind of vehicles, if
     * RestrParam is used, then restriction will be ignored by
     * specified types of vehicles.
     * </p>
     * <p>
     * [Emergency],[delivery],[car],[bus],[taxi],[pedestrian],[bicycle],[truck]
     * </p>
     * <p>
     * Example:
     * RestrParam=0,1,1,0
     * </p>
     * Above definition will set the restriction to be applied for
     * Emergency, Bus, Taxi, Pedestrian and Bicycle. Restriction
     * will NOT apply for Delivery and Car.
     *
     * @param value Tag value
     * @return the exceptMask in mkgmap internal format
     */

    private static byte getRestrictionExceptionMask(String value) {
        String[] params = value.split(",");
        byte exceptMask = 0x00;
        if (params.length > 0 && params.length <= 8) { // Got to have at least one parameter but not more than 8.
                        for (int i = 0; i < params.length; i++) {
                if ("1".equals(params[i])) {
                    switch(i) {
                                        case 0:
                                                exceptMask |= AccessTagsAndBits.EMERGENCY;
                                                break;
                                        case 1:
                                                exceptMask |= AccessTagsAndBits.DELIVERY;
                                                break;
                                        case 2:
                                                exceptMask |= AccessTagsAndBits.CAR;
                                                break;
                                        case 3:
                                                exceptMask |= AccessTagsAndBits.BUS;
                                                break;
                                        case 4:
                                                exceptMask |= AccessTagsAndBits.TAXI;
                                                break;
                                        case 5:
                                                exceptMask |= AccessTagsAndBits.FOOT;
                                                break;
                                        case 6:
                                                exceptMask |= AccessTagsAndBits.BIKE;
                                                break;
                                        case 7:
                                                exceptMask |= AccessTagsAndBits.TRUCK;
                                                break;
                    }
                }
            }
        } else {
            log.error("Invalid RestrParam definition. -> " + value);
        }

        return exceptMask;
    }

        @Override
        public int getPoiDispFlag() {
                return poiDispFlag;
        }

        public String getDefaultCountry() {
                return defaultCountry;
        }

        public String getDefaultRegion() {
                return defaultRegion;
        }

        private int decodeType(FeatureKind kind, String type) {
                try {
                        int t = Integer.decode(type);
                        if ((kind == FeatureKind.POLYGON || kind == FeatureKind.POLYLINE) && t >= 0x100 && t < 0x10000
                                        && (t & 0xff) == 0) {
                                // allow 0xYY00 instead of 0xYY
                                t >>= 8;
                        }
                        checkType(kind, t);
                        return t;
                } catch (NumberFormatException e) {
                        throw new MapFailedException("type not numeric " + type + " for " + kind + ", line " + lineNo);
                }
        }

       
        private void checkType(FeatureKind kind, int type) {
                if (kind == FeatureKind.POLYGON && type == 0x4a) // allow 0x4a polygon for preview map in polish format
                        return;
                if (!GType.checkType(kind, type)) {
                        throw new MapFailedException("invalid type " + GType.formatType(type) + " for " + kind + ", line " + lineNo);
                }
        }

}