package uk.me.parabola.mkgmap.osmstyle; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.general.LevelInfo; import uk.me.parabola.mkgmap.reader.osm.FeatureKind; import uk.me.parabola.mkgmap.reader.osm.GType; import uk.me.parabola.mkgmap.scan.SyntaxException; import uk.me.parabola.mkgmap.scan.TokType; import uk.me.parabola.mkgmap.scan.Token; import uk.me.parabola.mkgmap.scan.TokenScanner; /** * Read a type description from a style file. */ public class TypeReader { private static final Logger log = Logger.getLogger(TypeReader.class); private final FeatureKind kind; private final LevelInfo[] levels; private static final Pattern HYPHEN_PATTERN = Pattern.compile("-"); public TypeReader(FeatureKind kind, LevelInfo[] levels) { this.kind = kind; this.levels = levels; } public GType readType(TokenScanner ts){ return readType(ts, false, null); } public GType readType(TokenScanner ts, boolean performChecks, Map<Integer, List<Integer>> overlays) { // We should have a '[' to start with Token t = ts.nextToken(); if (t == null || t.getType() == TokType.EOF) throw new SyntaxException(ts, "No garmin type information given"); if (!t.getValue().equals("[")) { throw new SyntaxException(ts, "No type definition"); } ts.skipSpace(); String type = ts.nextValue(); if (!Character.isDigit(type.charAt(0))) throw new SyntaxException(ts, "Garmin type number must be first. Saw '" + type + '\''); log.debug("gtype", type); GType gt = new GType(kind, type); if (GType.checkType(gt.getFeatureKind(), gt.getType()) == false){ if (!performChecks && (kind != FeatureKind.POLYLINE || overlays == null || overlays.get(gt.getType()) == null)) throw new SyntaxException("invalid type " + type + " for " + kind + " in style file " + ts.getFileName() + ", line " + ts.getLinenumber()); } while (!ts.isEndOfFile()) { ts.skipSpace(); String w = ts.nextValue(); if (w.equals("]")) break; if (w.equals("level")) { setLevel(ts, gt); } else if (w.equals("resolution")) { setResolution(ts, gt); } else if (w.equals("default_name")) { gt.setDefaultName(nextValue(ts)); } else if (w.equals("road_class")) { gt.setRoadClass(nextIntValue(ts)); } else if (w.equals("road_speed")) { gt.setRoadSpeed(nextIntValue(ts)); } else if (w.equals("copy")) { // Reserved } else if (w.equals("continue")) { gt.setContinueSearch(true); // By default no propagate of actions on continue gt.propagateActions(false); } else if (w.equals("propagate") || w.equals("with_actions") || w.equals("withactions")) { gt.propagateActions(true); } else if (w.equals("no_propagate")) { gt.propagateActions(false); } else if (w.equals("oneway")) { // reserved } else if (w.equals("access")) { // reserved } else { throw new SyntaxException(ts, "Unrecognised type command '" + w + '\''); } } gt.fixLevels(levels); if ("lines".equals(ts.getFileName())){ if(gt.getRoadClass() < 0 || gt.getRoadClass() > 4) log.error("road class value", gt.getRoadClass(), "not in the range 0-4 in style file lines, line " + ts.getLinenumber()); if(gt.getRoadSpeed() < 0 || gt.getRoadSpeed() > 7) log.error("road speed value ", gt.getRoadSpeed(), "not in the range 0-7 in style file lines, line " + ts.getLinenumber()); } if (performChecks){ boolean fromOverlays = false; List<Integer> usedTypes = null; if (gt.getMaxResolution() < levels[0].getBits() || gt.getMaxResolution() > 24){ System.out.println("Warning: Object with max resolution of " + gt.getMaxResolution() + " is ignored. Check levels option and style file "+ ts.getFileName() + ", line " + ts.getLinenumber()); } else if (gt.getMinResolution() > 24) { System.out.println("Warning: Object with min resolution of " + gt.getMinResolution() + " is ignored. Check levels option and style file "+ ts.getFileName() + ", line " + ts.getLinenumber()); } if (overlays != null && kind == FeatureKind.POLYLINE){ usedTypes = overlays.get(gt.getType()); if (usedTypes != null) fromOverlays = true; } if (usedTypes == null) usedTypes = Arrays.asList(gt.getType()); boolean foundRoutableType = false; for (int i = 0; i < usedTypes.size(); i++){ int usedType = usedTypes.get(i); String typeOverlaidMsg = ". Type is overlaid with " + GType.formatType(usedType); if (GType.checkType(kind, usedType) == false){ String msg = "Warning: invalid type " + type + " for " + kind + " in style file " + ts.getFileName() + ", line " + ts.getLinenumber(); if (fromOverlays) msg += typeOverlaidMsg; System.out.println(msg); } if (kind == FeatureKind.POLYLINE && gt.getMinLevel() == 0 && gt.getMaxLevel() >= 0){ if (GType.isSpecialRoutableLineType(usedType)){ if (gt.hasRoadAttribute() == false){ String msg = "Warning: routable type " + type + " is used for non-routable line with level 0. This may break routing. Style file "+ ts.getFileName() + ", line " + ts.getLinenumber(); if (fromOverlays) msg += typeOverlaidMsg; System.out.println(msg); } else if (i > 0){ System.out.println("Warning: routable type " + type + " is used for non-routable line with level 0. " + "This may break routing. Style file " + ts.getFileName() + ", line " + ts.getLinenumber() + typeOverlaidMsg + " which is used for adding the non-routable copy of the way."); } } } if (kind == FeatureKind.POLYLINE && GType.isRoutableLineType(usedType)){ foundRoutableType = true; } } if (gt.hasRoadAttribute() && foundRoutableType == false && gt.getMinLevel() == 0 && gt.getMaxLevel() >= 0){ String msg = "Warning: non-routable type " + type + " is used in combination with road_class/road_speed. Line will not be routable. Style file "+ ts.getFileName() + ", line " + ts.getLinenumber(); if (fromOverlays) msg += ". Type is overlaid, but not with a routable type"; System.out.println(msg); } } return gt; } private static int nextIntValue(TokenScanner ts) { if (ts.checkToken("=")) ts.nextToken(); try { return ts.nextInt(); } catch (NumberFormatException e) { throw new SyntaxException(ts, "Expecting numeric value"); } } /** * Get the value in a 'name=value' pair. */ private static String nextValue(TokenScanner ts) { if (ts.checkToken("=")) ts.nextToken(); return ts.nextWord(); } /** * A resolution can be just a single number, in which case that is the * min resolution and the max defaults to 24. Or a min to max range. */ private static void setResolution(TokenScanner ts, GType gt) { String str = ts.nextWord(); log.debug("res word value", str); try { if (str.indexOf('-') >= 0) { String[] minmax = HYPHEN_PATTERN.split(str, 2); int val1 = Integer.parseInt(minmax[0]); int val2 = Integer.parseInt(minmax[1]); if (val1 > val2) { // Previously there was a bug where the order was reversed, so we swap the numbers if they are // the wrong way round. int h = val1; val1 = val2; val2 = h; } gt.setMinResolution(val1); gt.setMaxResolution(val2); } else { gt.setMinResolution(Integer.parseInt(str)); } } catch (NumberFormatException e) { throw new SyntaxException(ts, "Invalid value for resolution: '" + str + '\''); } } /** * Read a level spec, which is either the max level or a min to max range. * This is immediately converted to resolution(s). */ private void setLevel(TokenScanner ts, GType gt) { String str = ts.nextWord(); try { if (str.indexOf('-') >= 0) { String[] minmax = HYPHEN_PATTERN.split(str, 2); int val1 = toResolution(Integer.parseInt(minmax[0])); int val2 = toResolution(Integer.parseInt(minmax[1])); if (val1 > val2) { // Previously there was a bug where the order was reversed, so we swap the numbers if they are // the wrong way round. int h = val1; val1 = val2; val2 = h; } gt.setMinResolution(val1); gt.setMaxResolution(val2); } else { gt.setMinResolution(toResolution(Integer.parseInt(str))); } } catch (NumberFormatException e) { throw new SyntaxException(ts, "Invalid value for level: '" + str + '\''); } } private int toResolution(int level) { int max = levels.length - 1; if (level > max) throw new SyntaxException("Level number too large, max=" + max); return levels[max - level].getBits(); } }